import { f as feature, m as mesh, d as deviation, q as quantile, a as quantileSorted, b as median, c as mean, $ as $$1, p as piecewise, i as identity$1, l as log$3, e as pow$2, s as sqrt, g as symlog$1, u as utcTime, h as sequential, j as sequentialLog, k as sequentialPow, n as sequentialSqrt, o as sequentialSymlog, r as diverging, t as divergingLog, v as divergingPow, w as divergingSqrt, x as divergingSymlog, y as quantile$1, z as quantize$1, A as threshold, B as schemeSet3, C as schemeSet2, D as schemeSet1, E as schemePastel2, F as schemePastel1, G as schemePaired, H as schemeObservable10, I as schemeDark2, J as schemeCategory10, K as schemeAccent, L as area$2$1, S as Symbol$1, M as sum, N as geoTransverseMercator, O as geoStereographic, P as geoOrthographic, Q as geoNaturalEarth1, R as geoMollweide, T as geoMercator, U as geoIdentity, V as geoGnomonic, W as geoEquirectangular, X as geoEqualEarth, Y as geoConicEquidistant, Z as geoConicEqualArea, _ as geoConicConformal, a0 as geoAzimuthalEquidistant, a1 as geoAzimuthalEqualArea, a2 as geoAlbersUsa, a3 as geoAlbers, a4 as geoPath, a5 as graticule, a6 as forceSimulation, a7 as forceY, a8 as forceX, a9 as forceLink, aa as forceManyBody, ab as forceCollide, ac as forceCenter, ad as pack, ae as partition$4, af as stratify, ag as cluster, ah as tree$1, ai as treemapResquarify, aj as treemapSliceDice, ak as treemapBinary, al as Delaunay, am as permute, an as intersection, ao as union, ap as geoArea$1, aq as geoBounds$1, ar as geoCentroid$1, as as interval$1, at as stringify$2 } from './mollweide-Ca6MRGcL.js';
import { b as bisector, t as tickStep, p as precisionFixed, d as precisionRound, e as precisionPrefix, f as ascending$2, l as linear$1, g as tickFormat$1, h as bisectRight$1, i as interpolateRound, j as interpolate$1$1, k as bisectLeft$1 } from './linear-BFftg_y2.js';
import { p as millisecond, s as second, o as timeMinute, n as timeHour, l as timeDay, d as timeSunday, c as timeMonth, q as timeYear, u as utcMinute, r as utcHour, v as utcDay, w as utcSunday, x as utcMonth, y as utcYear, z as formatLocale$1, b as timeFormat$2, A as utcFormat$1, B as utcParse$1, C as timeParse$1, a as max$1, m as min$1, t as time$1, D as hcl, E as lab } from './time-uewm_MPu.js';
import { c as formatLocale, a as formatSpecifier, f as format$3, b as formatPrefix } from './defaultLocale-DG5JsUjF.js';
import { d as dsvFormat } from './dsv-BhAd467f.js';
import { r as range$4 } from './range-BRcAanmR.js';
import { o as ordinal, i as implicit } from './ordinal-mJAyPK2n.js';
import { D as stepBefore, E as stepAfter, F as curveStep, G as curveNatural, H as monotoneX, I as monotoneY, J as curveLinearClosed, K as curveLinear, L as curveCatmullRomOpen, M as curveCatmullRomClosed, N as curveCatmullRom, O as curveCardinalClosed, P as curveCardinalOpen, Q as curveCardinal, S as curveBundle, T as curveBasisOpen, U as curveBasisClosed, k as curveBasis, V as path$3, l as line$3, W as rgb, X as hsl } from './step-TZOpqHBK.js';
import { d as d3arc } from './arc-BEcVRQAW.js';
import { h as hierarchy, t as treemap, a as treemapSquarify, b as treemapSlice, c as treemapDice } from './index-OyOqWiks.js';
import './dispatch-tQmgj1It.js';
import './init-CT6u7j_0.js';
import './colors-CJG58WzC.js';

function accessor (fn, fields, name) {
  fn.fields = fields || [];
  fn.fname = name;
  return fn;
}
function accessorName(fn) {
  return fn == null ? null : fn.fname;
}
function accessorFields(fn) {
  return fn == null ? null : fn.fields;
}

function getter$1 (path) {
  return path.length === 1 ? get1(path[0]) : getN(path);
}
const get1 = field => function (obj) {
  return obj[field];
};
const getN = path => {
  const len = path.length;
  return function (obj) {
    for (let i = 0; i < len; ++i) {
      obj = obj[path[i]];
    }
    return obj;
  };
};

function error$1 (message) {
  throw Error(message);
}

function splitAccessPath (p) {
  const path = [],
    n = p.length;
  let q = null,
    b = 0,
    s = '',
    i,
    j,
    c;
  p = p + '';
  function push() {
    path.push(s + p.substring(i, j));
    s = '';
    i = j + 1;
  }
  for (i = j = 0; j < n; ++j) {
    c = p[j];
    if (c === '\\') {
      s += p.substring(i, j++);
      i = j;
    } else if (c === q) {
      push();
      q = null;
      b = -1;
    } else if (q) {
      continue;
    } else if (i === b && c === '"') {
      i = j + 1;
      q = c;
    } else if (i === b && c === "'") {
      i = j + 1;
      q = c;
    } else if (c === '.' && !b) {
      if (j > i) {
        push();
      } else {
        i = j + 1;
      }
    } else if (c === '[') {
      if (j > i) push();
      b = i = j + 1;
    } else if (c === ']') {
      if (!b) error$1('Access path missing open bracket: ' + p);
      if (b > 0) push();
      b = 0;
      i = j + 1;
    }
  }
  if (b) error$1('Access path missing closing bracket: ' + p);
  if (q) error$1('Access path missing closing quote: ' + p);
  if (j > i) {
    j++;
    push();
  }
  return path;
}

function field$1 (field, name, opt) {
  const path = splitAccessPath(field);
  field = path.length === 1 ? path[0] : field;
  return accessor((opt && opt.get || getter$1)(path), [field], name || field);
}

const id = field$1('id');
const identity = accessor(_ => _, [], 'identity');
const zero$2 = accessor(() => 0, [], 'zero');
const one$1 = accessor(() => 1, [], 'one');
const truthy = accessor(() => true, [], 'true');
const falsy = accessor(() => false, [], 'false');

/** Utilities common to vega-interpreter and vega-expression for evaluating expresions */

/** JSON authors are not allowed to set these properties, as these are built-in to the JS Object Prototype and should not be overridden. */
const DisallowedObjectProperties = new Set([...Object.getOwnPropertyNames(Object.prototype).filter(name => typeof Object.prototype[name] === 'function'), '__proto__']);

function log$1$1(method, level, input) {
  const args = [level].concat([].slice.call(input));
  console[method].apply(console, args); // eslint-disable-line no-console
}
const None$2 = 0;
const Error$1 = 1;
const Warn = 2;
const Info = 3;
const Debug = 4;
function logger (_, method, handler = log$1$1) {
  let level = _ || None$2;
  return {
    level(_) {
      if (arguments.length) {
        level = +_;
        return this;
      } else {
        return level;
      }
    },
    error() {
      if (level >= Error$1) handler(method || 'error', 'ERROR', arguments);
      return this;
    },
    warn() {
      if (level >= Warn) handler(method || 'warn', 'WARN', arguments);
      return this;
    },
    info() {
      if (level >= Info) handler(method || 'log', 'INFO', arguments);
      return this;
    },
    debug() {
      if (level >= Debug) handler(method || 'log', 'DEBUG', arguments);
      return this;
    }
  };
}

var isArray = Array.isArray;

function isObject (_) {
  return _ === Object(_);
}

const isLegalKey = key => key !== '__proto__';
function mergeConfig(...configs) {
  return configs.reduce((out, source) => {
    for (const key in source) {
      if (key === 'signals') {
        // for signals, we merge the signals arrays
        // source signals take precedence over
        // existing signals with the same name
        out.signals = mergeNamed(out.signals, source.signals);
      } else {
        // otherwise, merge objects subject to recursion constraints
        // for legend block, recurse for the layout entry only
        // for style block, recurse for all properties
        // otherwise, no recursion: objects overwrite, no merging
        const r = key === 'legend' ? {
          layout: 1
        } : key === 'style' ? true : null;
        writeConfig(out, key, source[key], r);
      }
    }
    return out;
  }, {});
}
function writeConfig(output, key, value, recurse) {
  if (!isLegalKey(key)) return;
  let k, o;
  if (isObject(value) && !isArray(value)) {
    o = isObject(output[key]) ? output[key] : output[key] = {};
    for (k in value) {
      if (recurse && (recurse === true || recurse[k])) {
        writeConfig(o, k, value[k]);
      } else if (isLegalKey(k)) {
        o[k] = value[k];
      }
    }
  } else {
    output[key] = value;
  }
}
function mergeNamed(a, b) {
  if (a == null) return b;
  const map = {},
    out = [];
  function add(_) {
    if (!map[_.name]) {
      map[_.name] = 1;
      out.push(_);
    }
  }
  b.forEach(add);
  a.forEach(add);
  return out;
}

function peek$1 (array) {
  return array[array.length - 1];
}

function toNumber (_) {
  return _ == null || _ === '' ? null : +_;
}

const exp$1 = sign => x => sign * Math.exp(x);
const log$2 = sign => x => Math.log(sign * x);
const symlog = c => x => Math.sign(x) * Math.log1p(Math.abs(x / c));
const symexp = c => x => Math.sign(x) * Math.expm1(Math.abs(x)) * c;
const pow$1 = exponent => x => x < 0 ? -Math.pow(-x, exponent) : Math.pow(x, exponent);
function pan(domain, delta, lift, ground) {
  const d0 = lift(domain[0]),
    d1 = lift(peek$1(domain)),
    dd = (d1 - d0) * delta;
  return [ground(d0 - dd), ground(d1 - dd)];
}
function panLinear(domain, delta) {
  return pan(domain, delta, toNumber, identity);
}
function panLog(domain, delta) {
  var sign = Math.sign(domain[0]);
  return pan(domain, delta, log$2(sign), exp$1(sign));
}
function panPow(domain, delta, exponent) {
  return pan(domain, delta, pow$1(exponent), pow$1(1 / exponent));
}
function panSymlog(domain, delta, constant) {
  return pan(domain, delta, symlog(constant), symexp(constant));
}
function zoom$1(domain, anchor, scale, lift, ground) {
  const d0 = lift(domain[0]),
    d1 = lift(peek$1(domain)),
    da = anchor != null ? lift(anchor) : (d0 + d1) / 2;
  return [ground(da + (d0 - da) * scale), ground(da + (d1 - da) * scale)];
}
function zoomLinear(domain, anchor, scale) {
  return zoom$1(domain, anchor, scale, toNumber, identity);
}
function zoomLog(domain, anchor, scale) {
  const sign = Math.sign(domain[0]);
  return zoom$1(domain, anchor, scale, log$2(sign), exp$1(sign));
}
function zoomPow(domain, anchor, scale, exponent) {
  return zoom$1(domain, anchor, scale, pow$1(exponent), pow$1(1 / exponent));
}
function zoomSymlog(domain, anchor, scale, constant) {
  return zoom$1(domain, anchor, scale, symlog(constant), symexp(constant));
}

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

function array$2 (_) {
  return _ != null ? isArray(_) ? _ : [_] : [];
}

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

function isFunction (_) {
  return typeof _ === 'function';
}

const DESCENDING = 'descending';
function compare$2 (fields, orders, opt) {
  opt = opt || {};
  orders = array$2(orders) || [];
  const ord = [],
    get = [],
    fmap = {},
    gen = opt.comparator || comparator$1;
  array$2(fields).forEach((f, i) => {
    if (f == null) return;
    ord.push(orders[i] === DESCENDING ? -1 : 1);
    get.push(f = isFunction(f) ? f : field$1(f, null, opt));
    (accessorFields(f) || []).forEach(_ => fmap[_] = 1);
  });
  return get.length === 0 ? null : accessor(gen(get, ord), Object.keys(fmap));
}
const ascending$1 = (u, v) => (u < v || u == null) && v != null ? -1 : (u > v || v == null) && u != null ? 1 : (v = v instanceof Date ? +v : v, u = u instanceof Date ? +u : u) !== u && v === v ? -1 : v !== v && u === u ? 1 : 0;
const comparator$1 = (fields, orders) => fields.length === 1 ? compare1(fields[0], orders[0]) : compareN(fields, orders, fields.length);
const compare1 = (field, order) => function (a, b) {
  return ascending$1(field(a), field(b)) * order;
};
const compareN = (fields, orders, n) => {
  orders.push(0); // pad zero for convenient lookup
  return function (a, b) {
    let f,
      c = 0,
      i = -1;
    while (c === 0 && ++i < n) {
      f = fields[i];
      c = ascending$1(f(a), f(b));
    }
    return c * orders[i];
  };
};

function constant$1 (_) {
  return isFunction(_) ? _ : () => _;
}

function debounce (delay, handler) {
  let tid;
  return e => {
    if (tid) clearTimeout(tid);
    tid = setTimeout(() => (handler(e), tid = null), delay);
  };
}

function extend (_) {
  for (let x, k, i = 1, len = arguments.length; i < len; ++i) {
    x = arguments[i];
    for (k in x) {
      _[k] = x[k];
    }
  }
  return _;
}

/**
 * Return an array with minimum and maximum values, in the
 * form [min, max]. Ignores null, undefined, and NaN values.
 */
function extent (array, f) {
  let i = 0,
    n,
    v,
    min,
    max;
  if (array && (n = array.length)) {
    if (f == null) {
      // find first valid value
      for (v = array[i]; i < n && (v == null || v !== v); v = array[++i]);
      min = max = v;

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

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

function extentIndex (array, f) {
  const n = array.length;
  let i = -1,
    a,
    b,
    c,
    u,
    v;
  if (f == null) {
    while (++i < n) {
      b = array[i];
      if (b != null && b >= b) {
        a = c = b;
        break;
      }
    }
    if (i === n) return [-1, -1];
    u = v = i;
    while (++i < n) {
      b = array[i];
      if (b != null) {
        if (a > b) {
          a = b;
          u = i;
        }
        if (c < b) {
          c = b;
          v = i;
        }
      }
    }
  } else {
    while (++i < n) {
      b = f(array[i], i, array);
      if (b != null && b >= b) {
        a = c = b;
        break;
      }
    }
    if (i === n) return [-1, -1];
    u = v = i;
    while (++i < n) {
      b = f(array[i], i, array);
      if (b != null) {
        if (a > b) {
          a = b;
          u = i;
        }
        if (c < b) {
          c = b;
          v = i;
        }
      }
    }
  }
  return [u, v];
}

function has$1 (object, property) {
  return Object.hasOwn(object, property);
}

const NULL = {};
function fastmap (input) {
  let obj = {},
    test;
  function has$1$1(key) {
    return has$1(obj, key) && obj[key] !== NULL;
  }
  const map = {
    size: 0,
    empty: 0,
    object: obj,
    has: has$1$1,
    get(key) {
      return has$1$1(key) ? obj[key] : undefined;
    },
    set(key, value) {
      if (!has$1$1(key)) {
        ++map.size;
        if (obj[key] === NULL) --map.empty;
      }
      obj[key] = value;
      return this;
    },
    delete(key) {
      if (has$1$1(key)) {
        --map.size;
        ++map.empty;
        obj[key] = NULL;
      }
      return this;
    },
    clear() {
      map.size = map.empty = 0;
      map.object = obj = {};
    },
    test(_) {
      if (arguments.length) {
        test = _;
        return map;
      } else {
        return test;
      }
    },
    clean() {
      const next = {};
      let size = 0;
      for (const key in obj) {
        const value = obj[key];
        if (value !== NULL && (!test || !test(value))) {
          next[key] = value;
          ++size;
        }
      }
      map.size = size;
      map.empty = 0;
      map.object = obj = next;
    }
  };
  if (input) Object.keys(input).forEach(key => {
    map.set(key, input[key]);
  });
  return map;
}

function flush (range, value, threshold, left, right, center) {
  if (!threshold && threshold !== 0) return center;
  const t = +threshold;
  let a = range[0],
    b = peek$1(range),
    l;

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

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

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

function inherits (child, parent, members) {
  const proto = child.prototype = Object.create(parent.prototype);
  Object.defineProperty(proto, 'constructor', {
    value: child,
    writable: true,
    enumerable: true,
    configurable: true
  });
  return extend(proto, members);
}

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

function isBoolean$2 (_) {
  return typeof _ === 'boolean';
}

function isDate$1 (_) {
  return Object.prototype.toString.call(_) === '[object Date]';
}

function isIterable (_) {
  return _ && isFunction(_[Symbol.iterator]);
}

function isNumber$1 (_) {
  return typeof _ === 'number';
}

function isRegExp (_) {
  return Object.prototype.toString.call(_) === '[object RegExp]';
}

function isString (_) {
  return typeof _ === 'string';
}

function key (fields, flat, opt) {
  if (fields) {
    fields = flat ? array$2(fields).map(f => f.replace(/\\(.)/g, '$1')) : array$2(fields);
  }
  const len = fields && fields.length,
    gen = opt && opt.get || getter$1,
    map = f => gen(flat ? [f] : splitAccessPath(f));
  let fn;
  if (!len) {
    fn = function () {
      return '';
    };
  } else if (len === 1) {
    const get = map(fields[0]);
    fn = function (_) {
      return '' + get(_);
    };
  } else {
    const get = fields.map(map);
    fn = function (_) {
      let s = '' + get[0](_),
        i = 0;
      while (++i < len) s += '|' + get[i](_);
      return s;
    };
  }
  return accessor(fn, fields, 'key');
}

function lerp (array, frac) {
  const lo = array[0],
    hi = peek$1(array),
    f = +frac;
  return !f ? lo : f === 1 ? hi : lo + f * (hi - lo);
}

const DEFAULT_MAX_SIZE = 10000;

// adapted from https://github.com/dominictarr/hashlru/ (MIT License)
function lruCache (maxsize) {
  maxsize = +maxsize || DEFAULT_MAX_SIZE;
  let curr, prev, size;
  const clear = () => {
    curr = {};
    prev = {};
    size = 0;
  };
  const update = (key, value) => {
    if (++size > maxsize) {
      prev = curr;
      curr = {};
      size = 1;
    }
    return curr[key] = value;
  };
  clear();
  return {
    clear,
    has: key => has$1(curr, key) || has$1(prev, key),
    get: key => has$1(curr, key) ? curr[key] : has$1(prev, key) ? update(key, prev[key]) : undefined,
    set: (key, value) => has$1(curr, key) ? curr[key] = value : update(key, value)
  };
}

function merge$2 (compare, array0, array1, output) {
  const n0 = array0.length,
    n1 = array1.length;
  if (!n1) return array0;
  if (!n0) return array1;
  const merged = output || new array0.constructor(n0 + n1);
  let i0 = 0,
    i1 = 0,
    i = 0;
  for (; i0 < n0 && i1 < n1; ++i) {
    merged[i] = compare(array0[i0], array1[i1]) > 0 ? array1[i1++] : array0[i0++];
  }
  for (; i0 < n0; ++i0, ++i) {
    merged[i] = array0[i0];
  }
  for (; i1 < n1; ++i1, ++i) {
    merged[i] = array1[i1];
  }
  return merged;
}

function repeat (str, reps) {
  let s = '';
  while (--reps >= 0) s += str;
  return s;
}

function pad (str, length, padchar, align) {
  const c = padchar || ' ',
    s = str + '',
    n = length - s.length;
  return n <= 0 ? s : align === 'left' ? repeat(c, n) + s : align === 'center' ? repeat(c, ~~(n / 2)) + s + repeat(c, Math.ceil(n / 2)) : s + repeat(c, n);
}

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

function $(x) {
  return isArray(x) ? `[${x.map(v => v === null ? 'null' : $(v))}]` : isObject(x) || isString(x) ?
  // Output valid JSON and JS source strings.
  // See https://github.com/judofyr/timeless/blob/master/posts/json-isnt-a-javascript-subset.md
  JSON.stringify(x).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029') : x;
}

function toBoolean (_) {
  return _ == null || _ === '' ? null : !_ || _ === 'false' || _ === '0' ? false : !!_;
}

const defaultParser = _ => isNumber$1(_) ? _ : isDate$1(_) ? _ : Date.parse(_);
function toDate (_, parser) {
  parser = parser || defaultParser;
  return _ == null || _ === '' ? null : parser(_);
}

function toString (_) {
  return _ == null || _ === '' ? null : _ + '';
}

function toSet (_) {
  const s = {},
    n = _.length;
  for (let i = 0; i < n; ++i) s[_[i]] = true;
  return s;
}

function truncate$1 (str, length, align, ellipsis) {
  const e = ellipsis != null ? ellipsis : '\u2026',
    s = str + '',
    n = s.length,
    l = Math.max(0, length - e.length);
  return n <= length ? s : align === 'left' ? e + s.slice(n - l) : align === 'center' ? s.slice(0, Math.ceil(l / 2)) + e + s.slice(n - ~~(l / 2)) : s.slice(0, l) + e;
}

function visitArray (array, filter, visitor) {
  if (array) {
    if (filter) {
      const n = array.length;
      for (let i = 0; i < n; ++i) {
        const t = filter(array[i]);
        if (t) visitor(t, i, array);
      }
    } else {
      array.forEach(visitor);
    }
  }
}

const YEAR = 'year';
const QUARTER = 'quarter';
const MONTH = 'month';
const WEEK = 'week';
const DATE = 'date';
const DAY = 'day';
const DAYOFYEAR = 'dayofyear';
const HOURS = 'hours';
const MINUTES = 'minutes';
const SECONDS = 'seconds';
const MILLISECONDS = 'milliseconds';
const TIME_UNITS = [YEAR, QUARTER, MONTH, WEEK, DATE, DAY, DAYOFYEAR, HOURS, MINUTES, SECONDS, MILLISECONDS];
const UNITS = TIME_UNITS.reduce((o, u, i) => (o[u] = 1 + i, o), {});
function timeUnits(units) {
  const u = array$2(units).slice(),
    m = {};

  // check validity
  if (!u.length) error$1('Missing time unit.');
  u.forEach(unit => {
    if (has$1(UNITS, unit)) {
      m[unit] = 1;
    } else {
      error$1(`Invalid time unit: ${unit}.`);
    }
  });
  const numTypes = (m[WEEK] || m[DAY] ? 1 : 0) + (m[QUARTER] || m[MONTH] || m[DATE] ? 1 : 0) + (m[DAYOFYEAR] ? 1 : 0);
  if (numTypes > 1) {
    error$1(`Incompatible time units: ${units}`);
  }

  // ensure proper sort order
  u.sort((a, b) => UNITS[a] - UNITS[b]);
  return u;
}
const defaultSpecifiers = {
  [YEAR]: '%Y ',
  [QUARTER]: 'Q%q ',
  [MONTH]: '%b ',
  [DATE]: '%d ',
  [WEEK]: 'W%U ',
  [DAY]: '%a ',
  [DAYOFYEAR]: '%j ',
  [HOURS]: '%H:00',
  [MINUTES]: '00:%M',
  [SECONDS]: ':%S',
  [MILLISECONDS]: '.%L',
  [`${YEAR}-${MONTH}`]: '%Y-%m ',
  [`${YEAR}-${MONTH}-${DATE}`]: '%Y-%m-%d ',
  [`${HOURS}-${MINUTES}`]: '%H:%M'
};
function timeUnitSpecifier(units, specifiers) {
  const s = extend({}, defaultSpecifiers, specifiers),
    u = timeUnits(units),
    n = u.length;
  let fmt = '',
    start = 0,
    end,
    key;
  for (start = 0; start < n;) {
    for (end = u.length; end > start; --end) {
      key = u.slice(start, end).join('-');
      if (s[key] != null) {
        fmt += s[key];
        start = end;
        break;
      }
    }
  }
  return fmt.trim();
}

const t0 = new Date();
function localYear(y) {
  t0.setFullYear(y);
  t0.setMonth(0);
  t0.setDate(1);
  t0.setHours(0, 0, 0, 0);
  return t0;
}
function dayofyear(d) {
  return localDayOfYear(new Date(d));
}
function week(d) {
  return localWeekNum(new Date(d));
}
function localDayOfYear(d) {
  return timeDay.count(localYear(d.getFullYear()) - 1, d);
}
function localWeekNum(d) {
  return timeSunday.count(localYear(d.getFullYear()) - 1, d);
}
function localFirst(y) {
  return localYear(y).getDay();
}
function localDate(y, m, d, H, M, S, L) {
  if (0 <= y && y < 100) {
    const date = new Date(-1, m, d, H, M, S, L);
    date.setFullYear(y);
    return date;
  }
  return new Date(y, m, d, H, M, S, L);
}
function utcdayofyear(d) {
  return utcDayOfYear(new Date(d));
}
function utcweek(d) {
  return utcWeekNum(new Date(d));
}
function utcDayOfYear(d) {
  const y = Date.UTC(d.getUTCFullYear(), 0, 1);
  return utcDay.count(y - 1, d);
}
function utcWeekNum(d) {
  const y = Date.UTC(d.getUTCFullYear(), 0, 1);
  return utcSunday.count(y - 1, d);
}
function utcFirst(y) {
  t0.setTime(Date.UTC(y, 0, 1));
  return t0.getUTCDay();
}
function utcDate(y, m, d, H, M, S, L) {
  if (0 <= y && y < 100) {
    const date = new Date(Date.UTC(-1, m, d, H, M, S, L));
    date.setUTCFullYear(d.y);
    return date;
  }
  return new Date(Date.UTC(y, m, d, H, M, S, L));
}

function floor(units, step, get, inv, newDate) {
  const s = step || 1,
    b = peek$1(units),
    _ = (unit, p, key) => {
      key = key || unit;
      return getUnit(get[key], inv[key], unit === b && s, p);
    };
  const t = new Date(),
    u = toSet(units),
    y = u[YEAR] ? _(YEAR) : constant$1(2012),
    m = u[MONTH] ? _(MONTH) : u[QUARTER] ? _(QUARTER) : zero$2,
    d = u[WEEK] && u[DAY] ? _(DAY, 1, WEEK + DAY) : u[WEEK] ? _(WEEK, 1) : u[DAY] ? _(DAY, 1) : u[DATE] ? _(DATE, 1) : u[DAYOFYEAR] ? _(DAYOFYEAR, 1) : one$1,
    H = u[HOURS] ? _(HOURS) : zero$2,
    M = u[MINUTES] ? _(MINUTES) : zero$2,
    S = u[SECONDS] ? _(SECONDS) : zero$2,
    L = u[MILLISECONDS] ? _(MILLISECONDS) : zero$2;
  return function (v) {
    t.setTime(+v);
    const year = y(t);
    return newDate(year, m(t), d(t, year), H(t), M(t), S(t), L(t));
  };
}
function getUnit(f, inv, step, phase) {
  const u = step <= 1 ? f : phase ? (d, y) => phase + step * Math.floor((f(d, y) - phase) / step) : (d, y) => step * Math.floor(f(d, y) / step);
  return inv ? (d, y) => inv(u(d, y), y) : u;
}

// returns the day of the year based on week number, day of week,
// and the day of the week for the first day of the year
function weekday(week, day, firstDay) {
  return day + week * 7 - (firstDay + 6) % 7;
}

// -- LOCAL TIME --

const localGet = {
  [YEAR]: d => d.getFullYear(),
  [QUARTER]: d => Math.floor(d.getMonth() / 3),
  [MONTH]: d => d.getMonth(),
  [DATE]: d => d.getDate(),
  [HOURS]: d => d.getHours(),
  [MINUTES]: d => d.getMinutes(),
  [SECONDS]: d => d.getSeconds(),
  [MILLISECONDS]: d => d.getMilliseconds(),
  [DAYOFYEAR]: d => localDayOfYear(d),
  [WEEK]: d => localWeekNum(d),
  [WEEK + DAY]: (d, y) => weekday(localWeekNum(d), d.getDay(), localFirst(y)),
  [DAY]: (d, y) => weekday(1, d.getDay(), localFirst(y))
};
const localInv = {
  [QUARTER]: q => 3 * q,
  [WEEK]: (w, y) => weekday(w, 0, localFirst(y))
};
function timeFloor(units, step) {
  return floor(units, step || 1, localGet, localInv, localDate);
}

// -- UTC TIME --

const utcGet = {
  [YEAR]: d => d.getUTCFullYear(),
  [QUARTER]: d => Math.floor(d.getUTCMonth() / 3),
  [MONTH]: d => d.getUTCMonth(),
  [DATE]: d => d.getUTCDate(),
  [HOURS]: d => d.getUTCHours(),
  [MINUTES]: d => d.getUTCMinutes(),
  [SECONDS]: d => d.getUTCSeconds(),
  [MILLISECONDS]: d => d.getUTCMilliseconds(),
  [DAYOFYEAR]: d => utcDayOfYear(d),
  [WEEK]: d => utcWeekNum(d),
  [DAY]: (d, y) => weekday(1, d.getUTCDay(), utcFirst(y)),
  [WEEK + DAY]: (d, y) => weekday(utcWeekNum(d), d.getUTCDay(), utcFirst(y))
};
const utcInv = {
  [QUARTER]: q => 3 * q,
  [WEEK]: (w, y) => weekday(w, 0, utcFirst(y))
};
function utcFloor(units, step) {
  return floor(units, step || 1, utcGet, utcInv, utcDate);
}

const timeIntervals = {
  [YEAR]: timeYear,
  [QUARTER]: timeMonth.every(3),
  [MONTH]: timeMonth,
  [WEEK]: timeSunday,
  [DATE]: timeDay,
  [DAY]: timeDay,
  [DAYOFYEAR]: timeDay,
  [HOURS]: timeHour,
  [MINUTES]: timeMinute,
  [SECONDS]: second,
  [MILLISECONDS]: millisecond
};
const utcIntervals = {
  [YEAR]: utcYear,
  [QUARTER]: utcMonth.every(3),
  [MONTH]: utcMonth,
  [WEEK]: utcSunday,
  [DATE]: utcDay,
  [DAY]: utcDay,
  [DAYOFYEAR]: utcDay,
  [HOURS]: utcHour,
  [MINUTES]: utcMinute,
  [SECONDS]: second,
  [MILLISECONDS]: millisecond
};
function timeInterval(unit) {
  return timeIntervals[unit];
}
function utcInterval(unit) {
  return utcIntervals[unit];
}
function offset$3(ival, date, step) {
  return ival ? ival.offset(date, step) : undefined;
}
function timeOffset(unit, date, step) {
  return offset$3(timeInterval(unit), date, step);
}
function utcOffset(unit, date, step) {
  return offset$3(utcInterval(unit), date, step);
}
function sequence$1(ival, start, stop, step) {
  return ival ? ival.range(start, stop, step) : undefined;
}
function timeSequence(unit, start, stop, step) {
  return sequence$1(timeInterval(unit), start, stop, step);
}
function utcSequence(unit, start, stop, step) {
  return sequence$1(utcInterval(unit), start, stop, step);
}

const durationSecond = 1000,
  durationMinute = durationSecond * 60,
  durationHour = durationMinute * 60,
  durationDay = durationHour * 24,
  durationWeek = durationDay * 7,
  durationMonth = durationDay * 30,
  durationYear = durationDay * 365;
const Milli = [YEAR, MONTH, DATE, HOURS, MINUTES, SECONDS, MILLISECONDS],
  Seconds = Milli.slice(0, -1),
  Minutes = Seconds.slice(0, -1),
  Hours = Minutes.slice(0, -1),
  Day = Hours.slice(0, -1),
  Week = [YEAR, WEEK],
  Month = [YEAR, MONTH],
  Year = [YEAR];
const intervals = [[Seconds, 1, durationSecond], [Seconds, 5, 5 * durationSecond], [Seconds, 15, 15 * durationSecond], [Seconds, 30, 30 * durationSecond], [Minutes, 1, durationMinute], [Minutes, 5, 5 * durationMinute], [Minutes, 15, 15 * durationMinute], [Minutes, 30, 30 * durationMinute], [Hours, 1, durationHour], [Hours, 3, 3 * durationHour], [Hours, 6, 6 * durationHour], [Hours, 12, 12 * durationHour], [Day, 1, durationDay], [Week, 1, durationWeek], [Month, 1, durationMonth], [Month, 3, 3 * durationMonth], [Year, 1, durationYear]];
function bin$1 (opt) {
  const ext = opt.extent,
    max = opt.maxbins || 40,
    target = Math.abs(span(ext)) / max;
  let i = bisector(i => i[2]).right(intervals, target),
    units,
    step;
  if (i === intervals.length) {
    units = Year, step = tickStep(ext[0] / durationYear, ext[1] / durationYear, max);
  } else if (i) {
    i = intervals[target / intervals[i - 1][2] < intervals[i][2] / target ? i - 1 : i];
    units = i[0];
    step = i[1];
  } else {
    units = Milli;
    step = Math.max(tickStep(ext[0], ext[1], max), 1);
  }
  return {
    units,
    step
  };
}

function memoize (method) {
  const cache = {};
  return spec => cache[spec] || (cache[spec] = method(spec));
}

function trimZeroes(numberFormat, decimalChar) {
  return x => {
    const str = numberFormat(x),
      dec = str.indexOf(decimalChar);
    if (dec < 0) return str;
    let idx = rightmostDigit(str, dec);
    const end = idx < str.length ? str.slice(idx) : '';
    while (--idx > dec) if (str[idx] !== '0') {
      ++idx;
      break;
    }
    return str.slice(0, idx) + end;
  };
}
function rightmostDigit(str, dec) {
  let i = str.lastIndexOf('e'),
    c;
  if (i > 0) return i;
  for (i = str.length; --i > dec;) {
    c = str.charCodeAt(i);
    if (c >= 48 && c <= 57) return i + 1; // is digit
  }
}
function numberLocale(locale) {
  const format = memoize(locale.format),
    formatPrefix = locale.formatPrefix;
  return {
    format,
    formatPrefix,
    formatFloat(spec) {
      const s = formatSpecifier(spec || ',');
      if (s.precision == null) {
        s.precision = 12;
        switch (s.type) {
          case '%':
            s.precision -= 2;
            break;
          case 'e':
            s.precision -= 1;
            break;
        }
        return trimZeroes(format(s),
        // number format
        format('.1f')(1)[1] // decimal point character
        );
      } else {
        return format(s);
      }
    },
    formatSpan(start, stop, count, specifier) {
      specifier = formatSpecifier(specifier == null ? ',f' : specifier);
      const step = tickStep(start, stop, count),
        value = Math.max(Math.abs(start), Math.abs(stop));
      let precision;
      if (specifier.precision == null) {
        switch (specifier.type) {
          case 's':
            {
              if (!isNaN(precision = precisionPrefix(step, value))) {
                specifier.precision = precision;
              }
              return formatPrefix(specifier, value);
            }
          case '':
          case 'e':
          case 'g':
          case 'p':
          case 'r':
            {
              if (!isNaN(precision = precisionRound(step, value))) {
                specifier.precision = precision - (specifier.type === 'e');
              }
              break;
            }
          case 'f':
          case '%':
            {
              if (!isNaN(precision = precisionFixed(step))) {
                specifier.precision = precision - (specifier.type === '%') * 2;
              }
              break;
            }
        }
      }
      return format(specifier);
    }
  };
}
let defaultNumberLocale;
resetNumberFormatDefaultLocale();
function resetNumberFormatDefaultLocale() {
  return defaultNumberLocale = numberLocale({
    format: format$3,
    formatPrefix: formatPrefix
  });
}
function numberFormatLocale(definition) {
  return numberLocale(formatLocale(definition));
}
function numberFormatDefaultLocale(definition) {
  return arguments.length ? defaultNumberLocale = numberFormatLocale(definition) : defaultNumberLocale;
}

function timeMultiFormat(format, interval, spec) {
  spec = spec || {};
  if (!isObject(spec)) {
    error$1(`Invalid time multi-format specifier: ${spec}`);
  }
  const second = interval(SECONDS),
    minute = interval(MINUTES),
    hour = interval(HOURS),
    day = interval(DATE),
    week = interval(WEEK),
    month = interval(MONTH),
    quarter = interval(QUARTER),
    year = interval(YEAR),
    L = format(spec[MILLISECONDS] || '.%L'),
    S = format(spec[SECONDS] || ':%S'),
    M = format(spec[MINUTES] || '%I:%M'),
    H = format(spec[HOURS] || '%I %p'),
    d = format(spec[DATE] || spec[DAY] || '%a %d'),
    w = format(spec[WEEK] || '%b %d'),
    m = format(spec[MONTH] || '%B'),
    q = format(spec[QUARTER] || '%B'),
    y = format(spec[YEAR] || '%Y');
  return date => (second(date) < date ? L : minute(date) < date ? S : hour(date) < date ? M : day(date) < date ? H : month(date) < date ? week(date) < date ? d : w : year(date) < date ? quarter(date) < date ? m : q : y)(date);
}
function timeLocale(locale) {
  const timeFormat = memoize(locale.format),
    utcFormat = memoize(locale.utcFormat);
  return {
    timeFormat: spec => isString(spec) ? timeFormat(spec) : timeMultiFormat(timeFormat, timeInterval, spec),
    utcFormat: spec => isString(spec) ? utcFormat(spec) : timeMultiFormat(utcFormat, utcInterval, spec),
    timeParse: memoize(locale.parse),
    utcParse: memoize(locale.utcParse)
  };
}
let defaultTimeLocale;
resetTimeFormatDefaultLocale();
function resetTimeFormatDefaultLocale() {
  return defaultTimeLocale = timeLocale({
    format: timeFormat$2,
    parse: timeParse$1,
    utcFormat: utcFormat$1,
    utcParse: utcParse$1
  });
}
function timeFormatLocale(definition) {
  return timeLocale(formatLocale$1(definition));
}
function timeFormatDefaultLocale(definition) {
  return arguments.length ? defaultTimeLocale = timeFormatLocale(definition) : defaultTimeLocale;
}

const createLocale = (number, time) => extend({}, number, time);
function locale(numberSpec, timeSpec) {
  const number = numberSpec ? numberFormatLocale(numberSpec) : numberFormatDefaultLocale();
  const time = timeSpec ? timeFormatLocale(timeSpec) : timeFormatDefaultLocale();
  return createLocale(number, time);
}
function defaultLocale(numberSpec, timeSpec) {
  const args = arguments.length;
  if (args && args !== 2) {
    error$1('defaultLocale expects either zero or two arguments.');
  }
  return args ? createLocale(numberFormatDefaultLocale(numberSpec), timeFormatDefaultLocale(timeSpec)) : createLocale(numberFormatDefaultLocale(), timeFormatDefaultLocale());
}
function resetDefaultLocale() {
  resetNumberFormatDefaultLocale();
  resetTimeFormatDefaultLocale();
  return defaultLocale();
}

// Matches absolute URLs with optional protocol
//   https://...    file://...    //...
const protocol_re = /^(data:|([A-Za-z]+:)?\/\/)/;

// Matches allowed URIs. From https://github.com/cure53/DOMPurify/blob/master/src/regexp.js with added file://
const allowed_re = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|file|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
const whitespace_re = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex

// Special treatment in node.js for the file: protocol
const fileProtocol = 'file://';

/**
 * Factory for a loader constructor that provides methods for requesting
 * files from either the network or disk, and for sanitizing request URIs.
 * @param {object} fs - The file system interface for file loading.
 *   If null or undefined, local file loading will be disabled.
 * @return {function} A loader constructor with the following signature:
 *   param {object} [options] - Optional default loading options to use.
 *   return {object} - A new loader instance.
 */
function loaderFactory (fs) {
  return options => ({
    options: options || {},
    sanitize: sanitize,
    load: load$1,
    fileAccess: false,
    file: fileLoader(),
    http: httpLoader
  });
}

/**
 * Load an external resource, typically either from the web or from the local
 * filesystem. This function uses {@link sanitize} to first sanitize the uri,
 * then calls either {@link http} (for web requests) or {@link file} (for
 * filesystem loading).
 * @param {string} uri - The resource indicator (e.g., URL or filename).
 * @param {object} [options] - Optional loading options. These options will
 *   override any existing default options.
 * @return {Promise} - A promise that resolves to the loaded content.
 */
async function load$1(uri, options) {
  const opt = await this.sanitize(uri, options),
    url = opt.href;
  return opt.localFile ? this.file(url) : this.http(url, options?.http);
}

/**
 * URI sanitizer function.
 * @param {string} uri - The uri (url or filename) to check.
 * @param {object} options - An options hash.
 * @return {Promise} - A promise that resolves to an object containing
 *  sanitized uri data, or rejects it the input uri is deemed invalid.
 *  The properties of the resolved object are assumed to be
 *  valid attributes for an HTML 'a' tag. The sanitized uri *must* be
 *  provided by the 'href' property of the returned object.
 */
async function sanitize(uri, options) {
  options = extend({}, this.options, options);
  const fileAccess = this.fileAccess,
    result = {
      href: null
    };
  let isFile, loadFile, base;
  const isAllowed = allowed_re.test(uri.replace(whitespace_re, ''));
  if (uri == null || typeof uri !== 'string' || !isAllowed) {
    error$1('Sanitize failure, invalid URI: ' + $(uri));
  }
  const hasProtocol = protocol_re.test(uri);

  // if relative url (no protocol/host), prepend baseURL
  if ((base = options.baseURL) && !hasProtocol) {
    // Ensure that there is a slash between the baseURL (e.g. hostname) and url
    if (!uri.startsWith('/') && !base.endsWith('/')) {
      uri = '/' + uri;
    }
    uri = base + uri;
  }

  // should we load from file system?
  loadFile = (isFile = uri.startsWith(fileProtocol)) || options.mode === 'file' || options.mode !== 'http' && !hasProtocol && fileAccess;
  if (isFile) {
    // strip file protocol
    uri = uri.slice(fileProtocol.length);
  } else if (uri.startsWith('//')) {
    if (options.defaultProtocol === 'file') {
      // if is file, strip protocol and set loadFile flag
      uri = uri.slice(2);
      loadFile = true;
    } else {
      // if relative protocol (starts with '//'), prepend default protocol
      uri = (options.defaultProtocol || 'http') + ':' + uri;
    }
  }

  // set non-enumerable mode flag to indicate local file load
  Object.defineProperty(result, 'localFile', {
    value: !!loadFile
  });

  // set uri
  result.href = uri;

  // set default result target, if specified
  if (options.target) {
    result.target = options.target + '';
  }

  // set default result rel, if specified (#1542)
  if (options.rel) {
    result.rel = options.rel + '';
  }

  // provide control over cross-origin image handling (#2238)
  // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
  if (options.context === 'image' && options.crossOrigin) {
    result.crossOrigin = options.crossOrigin + '';
  }

  // return
  return result;
}

/**
 * File system loader factory.
 * @param {object} fs - The file system interface.
 * @return {function} - A file loader with the following signature:
 *   param {string} filename - The file system path to load.
 *   param {string} filename - The file system path to load.
 *   return {Promise} A promise that resolves to the file contents.
 */
function fileLoader(fs) {
  return fileReject;
}

/**
 * Default file system loader that simply rejects.
 */
async function fileReject() {
  error$1('No file system access.');
}

/**
 * An http loader.
 * @param {string} url - The url to request.
 * @param {Partial<RequestInit>} options - An options hash.
 * @return {Promise} - A promise that resolves to the file contents.
 */
async function httpLoader(url, options) {
  const opt = extend({}, this.options.http, options),
    type = options && options.response,
    response = await fetch(url, opt);
  return !response.ok ? error$1(response.status + '' + response.statusText) : isFunction(response[type]) ? response[type]() : response.text();
}

const isValid = _ => _ != null && _ === _;
const isBoolean$1 = _ => _ === 'true' || _ === 'false' || _ === true || _ === false;
const isDate = _ => !Number.isNaN(Date.parse(_));
const isNumber = _ => !Number.isNaN(+_) && !(_ instanceof Date);
const isInteger$1 = _ => isNumber(_) && Number.isInteger(+_);
const typeParsers = {
  boolean: toBoolean,
  integer: toNumber,
  number: toNumber,
  date: toDate,
  string: toString,
  unknown: identity
};
const typeTests = [isBoolean$1, isInteger$1, isNumber, isDate];
const typeList = ['boolean', 'integer', 'number', 'date'];
function inferType(values, field) {
  if (!values || !values.length) return 'unknown';
  const n = values.length,
    m = typeTests.length,
    a = typeTests.map((_, i) => i + 1);
  for (let i = 0, t = 0, j, value; i < n; ++i) {
    value = field ? values[i][field] : values[i];
    for (j = 0; j < m; ++j) {
      if (a[j] && isValid(value) && !typeTests[j](value)) {
        a[j] = 0;
        ++t;
        if (t === typeTests.length) return 'string';
      }
    }
  }
  return typeList[a.reduce((u, v) => u === 0 ? v : u, 0) - 1];
}
function inferTypes(data, fields) {
  return fields.reduce((types, field) => {
    types[field] = inferType(data, field);
    return types;
  }, {});
}

function delimitedFormat(delimiter) {
  const parse = function (data, format) {
    const delim = {
      delimiter: delimiter
    };
    return dsv(data, format ? extend(format, delim) : delim);
  };
  parse.responseType = 'text';
  return parse;
}
function dsv(data, format) {
  if (format.header) {
    data = format.header.map($).join(format.delimiter) + '\n' + data;
  }
  return dsvFormat(format.delimiter).parse(data + '');
}
dsv.responseType = 'text';

function isBuffer(_) {
  return typeof Buffer === 'function' && isFunction(Buffer.isBuffer) ? Buffer.isBuffer(_) : false;
}
function json(data, format) {
  const prop = format && format.property ? field$1(format.property) : identity;
  return isObject(data) && !isBuffer(data) ? parseJSON(prop(data), format) : prop(JSON.parse(data));
}
json.responseType = 'json';
function parseJSON(data, format) {
  if (!isArray(data) && isIterable(data)) {
    data = [...data];
  }
  return format && format.copy ? JSON.parse(JSON.stringify(data)) : data;
}

const filters = {
  interior: (a, b) => a !== b,
  exterior: (a, b) => a === b
};
function topojson(data, format) {
  let method, object, property, filter;
  data = json(data, format);
  if (format && format.feature) {
    method = feature;
    property = format.feature;
  } else if (format && format.mesh) {
    method = mesh;
    property = format.mesh;
    filter = filters[format.filter];
  } else {
    error$1('Missing TopoJSON feature or mesh parameter.');
  }
  object = (object = data.objects[property]) ? method(data, object, filter) : error$1('Invalid TopoJSON object: ' + property);
  return object && object.features || [object];
}
topojson.responseType = 'json';

const format$2 = {
  dsv: dsv,
  csv: delimitedFormat(','),
  tsv: delimitedFormat('\t'),
  json: json,
  topojson: topojson
};
function formats$1(name, reader) {
  if (arguments.length > 1) {
    format$2[name] = reader;
    return this;
  } else {
    return has$1(format$2, name) ? format$2[name] : null;
  }
}
function responseType(type) {
  const f = formats$1(type);
  return f && f.responseType || 'text';
}

function read (data, schema, timeParser, utcParser) {
  schema = schema || {};
  const reader = formats$1(schema.type || 'json');
  if (!reader) error$1('Unknown data format type: ' + schema.type);
  data = reader(data, schema);
  if (schema.parse) parse$6(data, schema.parse, timeParser, utcParser);
  if (has$1(data, 'columns')) delete data.columns;
  return data;
}
function parse$6(data, types, timeParser, utcParser) {
  if (!data.length) return; // early exit for empty data

  const locale = timeFormatDefaultLocale();
  timeParser = timeParser || locale.timeParse;
  utcParser = utcParser || locale.utcParse;
  let fields = data.columns || Object.keys(data[0]),
    datum,
    field,
    i,
    j,
    n,
    m;
  if (types === 'auto') types = inferTypes(data, fields);
  fields = Object.keys(types);
  const parsers = fields.map(field => {
    const type = types[field];
    let parts, pattern;
    if (type && (type.startsWith('date:') || type.startsWith('utc:'))) {
      parts = type.split(/:(.+)?/, 2); // split on first :
      pattern = parts[1];
      if (pattern[0] === '\'' && pattern[pattern.length - 1] === '\'' || pattern[0] === '"' && pattern[pattern.length - 1] === '"') {
        pattern = pattern.slice(1, -1);
      }
      const parse = parts[0] === 'utc' ? utcParser : timeParser;
      return parse(pattern);
    }
    if (!typeParsers[type]) {
      throw Error('Illegal format pattern: ' + field + ':' + type);
    }
    return typeParsers[type];
  });
  for (i = 0, n = data.length, m = fields.length; i < n; ++i) {
    datum = data[i];
    for (j = 0; j < m; ++j) {
      field = fields[j];
      datum[field] = parsers[j](datum[field]);
    }
  }
}

const loader = loaderFactory();

function UniqueList(idFunc) {
  const $ = idFunc || identity,
    list = [],
    ids = {};
  list.add = _ => {
    const id = $(_);
    if (!ids[id]) {
      ids[id] = 1;
      list.push(_);
    }
    return list;
  };
  list.remove = _ => {
    const id = $(_);
    if (ids[id]) {
      ids[id] = 0;
      const idx = list.indexOf(_);
      if (idx >= 0) list.splice(idx, 1);
    }
    return list;
  };
  return list;
}

/**
 * Invoke and await a potentially async callback function. If
 * an error occurs, trap it and route to Dataflow.error.
 * @param {Dataflow} df - The dataflow instance
 * @param {function} callback - A callback function to invoke
 *   and then await. The dataflow will be passed as the single
 *   argument to the function.
 */
async function asyncCallback (df, callback) {
  try {
    await callback(df);
  } catch (err) {
    df.error(err);
  }
}

const TUPLE_ID_KEY = Symbol('vega_id');
let TUPLE_ID = 1;

/**
 * Checks if an input value is a registered tuple.
 * @param {*} t - The value to check.
 * @return {boolean} True if the input is a tuple, false otherwise.
 */
function isTuple(t) {
  return !!(t && tupleid(t));
}

/**
 * Returns the id of a tuple.
 * @param {object} t - The input tuple.
 * @return {*} the tuple id.
 */
function tupleid(t) {
  return t[TUPLE_ID_KEY];
}

/**
 * Sets the id of a tuple.
 * @param {object} t - The input tuple.
 * @param {*} id - The id value to set.
 * @return {object} the input tuple.
 */
function setid(t, id) {
  t[TUPLE_ID_KEY] = id;
  return t;
}

/**
 * Ingest an object or value as a data tuple.
 * If the input value is an object, an id field will be added to it. For
 * efficiency, the input object is modified directly. A copy is not made.
 * If the input value is a literal, it will be wrapped in a new object
 * instance, with the value accessible as the 'data' property.
 * @param datum - The value to ingest.
 * @return {object} The ingested data tuple.
 */
function ingest$1(datum) {
  const t = datum === Object(datum) ? datum : {
    data: datum
  };
  return tupleid(t) ? t : setid(t, TUPLE_ID++);
}

/**
 * Given a source tuple, return a derived copy.
 * @param {object} t - The source tuple.
 * @return {object} The derived tuple.
 */
function derive(t) {
  return rederive(t, ingest$1({}));
}

/**
 * Rederive a derived tuple by copying values from the source tuple.
 * @param {object} t - The source tuple.
 * @param {object} d - The derived tuple.
 * @return {object} The derived tuple.
 */
function rederive(t, d) {
  for (const k in t) d[k] = t[k];
  return d;
}

/**
 * Replace an existing tuple with a new tuple.
 * @param {object} t - The existing data tuple.
 * @param {object} d - The new tuple that replaces the old.
 * @return {object} The new tuple.
 */
function replace$1(t, d) {
  return setid(d, tupleid(t));
}

/**
 * Generate an augmented comparator function that provides stable
 * sorting by tuple id when the given comparator produces ties.
 * @param {function} cmp - The comparator to augment.
 * @param {function} [f] - Optional tuple accessor function.
 * @return {function} An augmented comparator function.
 */
function stableCompare(cmp, f) {
  return !cmp ? null : f ? (a, b) => cmp(a, b) || tupleid(f(a)) - tupleid(f(b)) : (a, b) => cmp(a, b) || tupleid(a) - tupleid(b);
}

function isChangeSet(v) {
  return v && v.constructor === changeset;
}
function changeset() {
  const add = [],
    // insert tuples
    rem = [],
    // remove tuples
    mod = [],
    // modify tuples
    remp = [],
    // remove by predicate
    modp = []; // modify by predicate
  let clean = null,
    reflow = false;
  return {
    constructor: changeset,
    insert(t) {
      const d = array$2(t),
        n = d.length;
      for (let i = 0; i < n; ++i) add.push(d[i]);
      return this;
    },
    remove(t) {
      const a = isFunction(t) ? remp : rem,
        d = array$2(t),
        n = d.length;
      for (let i = 0; i < n; ++i) a.push(d[i]);
      return this;
    },
    modify(t, field, value) {
      const m = {
        field: field,
        value: constant$1(value)
      };
      if (isFunction(t)) {
        m.filter = t;
        modp.push(m);
      } else {
        m.tuple = t;
        mod.push(m);
      }
      return this;
    },
    encode(t, set) {
      if (isFunction(t)) modp.push({
        filter: t,
        field: set
      });else mod.push({
        tuple: t,
        field: set
      });
      return this;
    },
    clean(value) {
      clean = value;
      return this;
    },
    reflow() {
      reflow = true;
      return this;
    },
    pulse(pulse, tuples) {
      const cur = {},
        out = {};
      let i, n, m, f, t, id;

      // build lookup table of current tuples
      for (i = 0, n = tuples.length; i < n; ++i) {
        cur[tupleid(tuples[i])] = 1;
      }

      // process individual tuples to remove
      for (i = 0, n = rem.length; i < n; ++i) {
        t = rem[i];
        cur[tupleid(t)] = -1;
      }

      // process predicate-based removals
      for (i = 0, n = remp.length; i < n; ++i) {
        f = remp[i];
        tuples.forEach(t => {
          if (f(t)) cur[tupleid(t)] = -1;
        });
      }

      // process all add tuples
      for (i = 0, n = add.length; i < n; ++i) {
        t = add[i];
        id = tupleid(t);
        if (cur[id]) {
          // tuple already resides in dataset
          // if flagged for both add and remove, cancel
          cur[id] = 1;
        } else {
          // tuple does not reside in dataset, add
          pulse.add.push(ingest$1(add[i]));
        }
      }

      // populate pulse rem list
      for (i = 0, n = tuples.length; i < n; ++i) {
        t = tuples[i];
        if (cur[tupleid(t)] < 0) pulse.rem.push(t);
      }

      // modify helper method
      function modify(t, f, v) {
        if (v) {
          t[f] = v(t);
        } else {
          pulse.encode = f;
        }
        if (!reflow) out[tupleid(t)] = t;
      }

      // process individual tuples to modify
      for (i = 0, n = mod.length; i < n; ++i) {
        m = mod[i];
        t = m.tuple;
        f = m.field;
        id = cur[tupleid(t)];
        if (id > 0) {
          modify(t, f, m.value);
          pulse.modifies(f);
        }
      }

      // process predicate-based modifications
      for (i = 0, n = modp.length; i < n; ++i) {
        m = modp[i];
        f = m.filter;
        tuples.forEach(t => {
          if (f(t) && cur[tupleid(t)] > 0) {
            modify(t, m.field, m.value);
          }
        });
        pulse.modifies(m.field);
      }

      // upon reflow request, populate mod with all non-removed tuples
      // otherwise, populate mod with modified tuples only
      if (reflow) {
        pulse.mod = rem.length || remp.length ? tuples.filter(t => cur[tupleid(t)] > 0) : tuples.slice();
      } else {
        for (id in out) pulse.mod.push(out[id]);
      }

      // set pulse garbage collection request
      if (clean || clean == null && (rem.length || remp.length)) {
        pulse.clean(true);
      }
      return pulse;
    }
  };
}

const CACHE = '_:mod:_';

/**
 * Hash that tracks modifications to assigned values.
 * Callers *must* use the set method to update values.
 */
function Parameters() {
  Object.defineProperty(this, CACHE, {
    writable: true,
    value: {}
  });
}
Parameters.prototype = {
  /**
   * Set a parameter value. If the parameter value changes, the parameter
   * will be recorded as modified.
   * @param {string} name - The parameter name.
   * @param {number} index - The index into an array-value parameter. Ignored if
   *   the argument is undefined, null or less than zero.
   * @param {*} value - The parameter value to set.
   * @param {boolean} [force=false] - If true, records the parameter as modified
   *   even if the value is unchanged.
   * @return {Parameters} - This parameter object.
   */
  set(name, index, value, force) {
    const o = this,
      v = o[name],
      mod = o[CACHE];
    if (index != null && index >= 0) {
      if (v[index] !== value || force) {
        v[index] = value;
        mod[index + ':' + name] = -1;
        mod[name] = -1;
      }
    } else if (v !== value || force) {
      o[name] = value;
      mod[name] = isArray(value) ? 1 + value.length : -1;
    }
    return o;
  },
  /**
   * Tests if one or more parameters has been modified. If invoked with no
   * arguments, returns true if any parameter value has changed. If the first
   * argument is array, returns trues if any parameter name in the array has
   * changed. Otherwise, tests if the given name and optional array index has
   * changed.
   * @param {string} name - The parameter name to test.
   * @param {number} [index=undefined] - The parameter array index to test.
   * @return {boolean} - Returns true if a queried parameter was modified.
   */
  modified(name, index) {
    const mod = this[CACHE];
    if (!arguments.length) {
      for (const k in mod) {
        if (mod[k]) return true;
      }
      return false;
    } else if (isArray(name)) {
      for (let k = 0; k < name.length; ++k) {
        if (mod[name[k]]) return true;
      }
      return false;
    }
    return index != null && index >= 0 ? index + 1 < mod[name] || !!mod[index + ':' + name] : !!mod[name];
  },
  /**
   * Clears the modification records. After calling this method,
   * all parameters are considered unmodified.
   */
  clear() {
    this[CACHE] = {};
    return this;
  }
};

let OP_ID = 0;
const PULSE = 'pulse',
  NO_PARAMS = new Parameters();

// Boolean Flags
const SKIP$1$1 = 1,
  MODIFIED = 2;

/**
 * An Operator is a processing node in a dataflow graph.
 * Each operator stores a value and an optional value update function.
 * Operators can accept a hash of named parameters. Parameter values can
 * either be direct (JavaScript literals, arrays, objects) or indirect
 * (other operators whose values will be pulled dynamically). Operators
 * included as parameters will have this operator added as a dependency.
 * @constructor
 * @param {*} [init] - The initial value for this operator.
 * @param {function(object, Pulse)} [update] - An update function. Upon
 *   evaluation of this operator, the update function will be invoked and the
 *   return value will be used as the new value of this operator.
 * @param {object} [params] - The parameters for this operator.
 * @param {boolean} [react=true] - Flag indicating if this operator should
 *   listen for changes to upstream operators included as parameters.
 * @see parameters
 */
function Operator(init, update, params, react) {
  this.id = ++OP_ID;
  this.value = init;
  this.stamp = -1;
  this.rank = -1;
  this.qrank = -1;
  this.flags = 0;
  if (update) {
    this._update = update;
  }
  if (params) this.parameters(params, react);
}
function flag(bit) {
  return function (state) {
    const f = this.flags;
    if (arguments.length === 0) return !!(f & bit);
    this.flags = state ? f | bit : f & ~bit;
    return this;
  };
}
Operator.prototype = {
  /**
   * Returns a list of target operators dependent on this operator.
   * If this list does not exist, it is created and then returned.
   * @return {UniqueList}
   */
  targets() {
    return this._targets || (this._targets = UniqueList(id));
  },
  /**
   * Sets the value of this operator.
   * @param {*} value - the value to set.
   * @return {Number} Returns 1 if the operator value has changed
   *   according to strict equality, returns 0 otherwise.
   */
  set(value) {
    if (this.value !== value) {
      this.value = value;
      return 1;
    } else {
      return 0;
    }
  },
  /**
   * Indicates that operator evaluation should be skipped on the next pulse.
   * This operator will still propagate incoming pulses, but its update function
   * will not be invoked. The skip flag is reset after every pulse, so calling
   * this method will affect processing of the next pulse only.
   */
  skip: flag(SKIP$1$1),
  /**
   * Indicates that this operator's value has been modified on its most recent
   * pulse. Normally modification is checked via strict equality; however, in
   * some cases it is more efficient to update the internal state of an object.
   * In those cases, the modified flag can be used to trigger propagation. Once
   * set, the modification flag persists across pulses until unset. The flag can
   * be used with the last timestamp to test if a modification is recent.
   */
  modified: flag(MODIFIED),
  /**
   * Sets the parameters for this operator. The parameter values are analyzed for
   * operator instances. If found, this operator will be added as a dependency
   * of the parameterizing operator. Operator values are dynamically marshalled
   * from each operator parameter prior to evaluation. If a parameter value is
   * an array, the array will also be searched for Operator instances. However,
   * the search does not recurse into sub-arrays or object properties.
   * @param {object} params - A hash of operator parameters.
   * @param {boolean} [react=true] - A flag indicating if this operator should
   *   automatically update (react) when parameter values change. In other words,
   *   this flag determines if the operator registers itself as a listener on
   *   any upstream operators included in the parameters.
   * @param {boolean} [initonly=false] - A flag indicating if this operator
   *   should calculate an update only upon its initial evaluation, then
   *   deregister dependencies and suppress all future update invocations.
   * @return {Operator[]} - An array of upstream dependencies.
   */
  parameters(params, react, initonly) {
    react = react !== false;
    const argval = this._argval = this._argval || new Parameters(),
      argops = this._argops = this._argops || [],
      deps = [];
    let name, value, n, i;
    const add = (name, index, value) => {
      if (value instanceof Operator) {
        if (value !== this) {
          if (react) value.targets().add(this);
          deps.push(value);
        }
        argops.push({
          op: value,
          name: name,
          index: index
        });
      } else {
        argval.set(name, index, value);
      }
    };
    for (name in params) {
      value = params[name];
      if (name === PULSE) {
        array$2(value).forEach(op => {
          if (!(op instanceof Operator)) {
            error$1('Pulse parameters must be operator instances.');
          } else if (op !== this) {
            op.targets().add(this);
            deps.push(op);
          }
        });
        this.source = value;
      } else if (isArray(value)) {
        argval.set(name, -1, Array(n = value.length));
        for (i = 0; i < n; ++i) add(name, i, value[i]);
      } else {
        add(name, -1, value);
      }
    }
    this.marshall().clear(); // initialize values
    if (initonly) argops.initonly = true;
    return deps;
  },
  /**
   * Internal method for marshalling parameter values.
   * Visits each operator dependency to pull the latest value.
   * @return {Parameters} A Parameters object to pass to the update function.
   */
  marshall(stamp) {
    const argval = this._argval || NO_PARAMS,
      argops = this._argops;
    let item, i, op, mod;
    if (argops) {
      const n = argops.length;
      for (i = 0; i < n; ++i) {
        item = argops[i];
        op = item.op;
        mod = op.modified() && op.stamp === stamp;
        argval.set(item.name, item.index, op.value, mod);
      }
      if (argops.initonly) {
        for (i = 0; i < n; ++i) {
          item = argops[i];
          item.op.targets().remove(this);
        }
        this._argops = null;
        this._update = null;
      }
    }
    return argval;
  },
  /**
   * Detach this operator from the dataflow.
   * Unregisters listeners on upstream dependencies.
   */
  detach() {
    const argops = this._argops;
    let i, n, item, op;
    if (argops) {
      for (i = 0, n = argops.length; i < n; ++i) {
        item = argops[i];
        op = item.op;
        if (op._targets) {
          op._targets.remove(this);
        }
      }
    }

    // remove references to the source and pulse object,
    // if present, to prevent memory leaks of old data.
    this.pulse = null;
    this.source = null;
  },
  /**
   * Delegate method to perform operator processing.
   * Subclasses can override this method to perform custom processing.
   * By default, it marshalls parameters and calls the update function
   * if that function is defined. If the update function does not
   * change the operator value then StopPropagation is returned.
   * If no update function is defined, this method does nothing.
   * @param {Pulse} pulse - the current dataflow pulse.
   * @return The output pulse or StopPropagation. A falsy return value
   *   (including undefined) will let the input pulse pass through.
   */
  evaluate(pulse) {
    const update = this._update;
    if (update) {
      const params = this.marshall(pulse.stamp),
        v = update.call(this, params, pulse);
      params.clear();
      if (v !== this.value) {
        this.value = v;
      } else if (!this.modified()) {
        return pulse.StopPropagation;
      }
    }
  },
  /**
   * Run this operator for the current pulse. If this operator has already
   * been run at (or after) the pulse timestamp, returns StopPropagation.
   * Internally, this method calls {@link evaluate} to perform processing.
   * If {@link evaluate} returns a falsy value, the input pulse is returned.
   * This method should NOT be overridden, instead overrride {@link evaluate}.
   * @param {Pulse} pulse - the current dataflow pulse.
   * @return the output pulse for this operator (or StopPropagation)
   */
  run(pulse) {
    if (pulse.stamp < this.stamp) return pulse.StopPropagation;
    let rv;
    if (this.skip()) {
      this.skip(false);
      rv = 0;
    } else {
      rv = this.evaluate(pulse);
    }
    return this.pulse = rv || pulse;
  }
};

/**
 * Add an operator to the dataflow graph. This function accepts a
 * variety of input argument types. The basic signature supports an
 * initial value, update function and parameters. If the first parameter
 * is an Operator instance, it will be added directly. If it is a
 * constructor for an Operator subclass, a new instance will be instantiated.
 * Otherwise, if the first parameter is a function instance, it will be used
 * as the update function and a null initial value is assumed.
 * @param {*} init - One of: the operator to add, the initial value of
 *   the operator, an operator class to instantiate, or an update function.
 * @param {function} [update] - The operator update function.
 * @param {object} [params] - The operator parameters.
 * @param {boolean} [react=true] - Flag indicating if this operator should
 *   listen for changes to upstream operators included as parameters.
 * @return {Operator} - The added operator.
 */
function add$3 (init, update, params, react) {
  let shift = 1,
    op;
  if (init instanceof Operator) {
    op = init;
  } else if (init && init.prototype instanceof Operator) {
    op = new init();
  } else if (isFunction(init)) {
    op = new Operator(null, init);
  } else {
    shift = 0;
    op = new Operator(init, update);
  }
  this.rank(op);
  if (shift) {
    react = params;
    params = update;
  }
  if (params) this.connect(op, op.parameters(params, react));
  this.touch(op);
  return op;
}

/**
 * Connect a target operator as a dependent of source operators.
 * If necessary, this method will rerank the target operator and its
 * dependents to ensure propagation proceeds in a topologically sorted order.
 * @param {Operator} target - The target operator.
 * @param {Array<Operator>} - The source operators that should propagate
 *   to the target operator.
 */
function connect (target, sources) {
  const targetRank = target.rank,
    n = sources.length;
  for (let i = 0; i < n; ++i) {
    if (targetRank < sources[i].rank) {
      this.rerank(target);
      return;
    }
  }
}

let STREAM_ID = 0;

/**
 * Models an event stream.
 * @constructor
 * @param {function(Object, number): boolean} [filter] - Filter predicate.
 *   Events pass through when truthy, events are suppressed when falsy.
 * @param {function(Object): *} [apply] - Applied to input events to produce
 *   new event values.
 * @param {function(Object)} [receive] - Event callback function to invoke
 *   upon receipt of a new event. Use to override standard event processing.
 */
function EventStream(filter, apply, receive) {
  this.id = ++STREAM_ID;
  this.value = null;
  if (receive) this.receive = receive;
  if (filter) this._filter = filter;
  if (apply) this._apply = apply;
}

/**
 * Creates a new event stream instance with the provided
 * (optional) filter, apply and receive functions.
 * @param {function(Object, number): boolean} [filter] - Filter predicate.
 *   Events pass through when truthy, events are suppressed when falsy.
 * @param {function(Object): *} [apply] - Applied to input events to produce
 *   new event values.
 * @see EventStream
 */
function stream(filter, apply, receive) {
  return new EventStream(filter, apply, receive);
}
EventStream.prototype = {
  _filter: truthy,
  _apply: identity,
  targets() {
    return this._targets || (this._targets = UniqueList(id));
  },
  consume(_) {
    if (!arguments.length) return !!this._consume;
    this._consume = !!_;
    return this;
  },
  receive(evt) {
    if (this._filter(evt)) {
      const val = this.value = this._apply(evt),
        trg = this._targets,
        n = trg ? trg.length : 0;
      for (let i = 0; i < n; ++i) trg[i].receive(val);
      if (this._consume) {
        evt.preventDefault();
        evt.stopPropagation();
      }
    }
  },
  filter(filter) {
    const s = stream(filter);
    this.targets().add(s);
    return s;
  },
  apply(apply) {
    const s = stream(null, apply);
    this.targets().add(s);
    return s;
  },
  merge() {
    const s = stream();
    this.targets().add(s);
    for (let i = 0, n = arguments.length; i < n; ++i) {
      arguments[i].targets().add(s);
    }
    return s;
  },
  throttle(pause) {
    let t = -1;
    return this.filter(() => {
      const now = Date.now();
      if (now - t > pause) {
        t = now;
        return 1;
      } else {
        return 0;
      }
    });
  },
  debounce(delay) {
    const s = stream();
    this.targets().add(stream(null, null, debounce(delay, e => {
      const df = e.dataflow;
      s.receive(e);
      if (df && df.run) df.run();
    })));
    return s;
  },
  between(a, b) {
    let active = false;
    a.targets().add(stream(null, null, () => active = true));
    b.targets().add(stream(null, null, () => active = false));
    return this.filter(() => active);
  },
  detach() {
    // ensures compatibility with operators (#2753)
    // remove references to other streams and filter functions that may
    // be bound to subcontexts that need to be garbage collected.
    this._filter = truthy;
    this._targets = null;
  }
};

/**
 * Create a new event stream from an event source.
 * @param {object} source - The event source to monitor. The input must
 *  support the addEventListener method.
 * @param {string} type - The event type.
 * @param {function(object): boolean} [filter] - Event filter function.
 * @param {function(object): *} [apply] - Event application function.
 *   If provided, this function will be invoked and the result will be
 *   used as the downstream event value.
 * @return {EventStream}
 */
function events$1 (source, type, filter, apply) {
  const df = this,
    s = stream(filter, apply),
    send = function (e) {
      e.dataflow = df;
      try {
        s.receive(e);
      } catch (error) {
        df.error(error);
      } finally {
        df.run();
      }
    };
  let sources;
  if (typeof source === 'string' && typeof document !== 'undefined') {
    sources = document.querySelectorAll(source);
  } else {
    sources = array$2(source);
  }
  const n = sources.length;
  for (let i = 0; i < n; ++i) {
    sources[i].addEventListener(type, send);
  }
  return s;
}

function parse$5(data, format) {
  const locale = this.locale();
  return read(data, format, locale.timeParse, locale.utcParse);
}

/**
 * Ingests new data into the dataflow. First parses the data using the
 * vega-loader read method, then pulses a changeset to the target operator.
 * @param {Operator} target - The Operator to target with ingested data,
 *   typically a Collect transform instance.
 * @param {*} data - The input data, prior to parsing. For JSON this may
 *   be a string or an object. For CSV, TSV, etc should be a string.
 * @param {object} format - The data format description for parsing
 *   loaded data. This object is passed to the vega-loader read method.
 * @returns {Dataflow}
 */
function ingest(target, data, format) {
  data = this.parse(data, format);
  return this.pulse(target, this.changeset().insert(data));
}

/**
 * Request data from an external source, parse it, and return a Promise.
 * @param {string} url - The URL from which to load the data. This string
 *   is passed to the vega-loader load method.
 * @param {object} [format] - The data format description for parsing
 *   loaded data. This object is passed to the vega-loader read method.
 * @return {Promise} A Promise that resolves upon completion of the request.
 *   The resolved object contains the following properties:
 *   - data: an array of parsed data (or null upon error)
 *   - status: a code for success (0), load fail (-1), or parse fail (-2)
 */
async function request(url, format) {
  const df = this;
  let status = 0,
    data;
  try {
    data = await df.loader().load(url, {
      context: 'dataflow',
      response: responseType(format && format.type)
    });
    try {
      data = df.parse(data, format);
    } catch (err) {
      status = -2;
      df.warn('Data ingestion failed', url, err);
    }
  } catch (err) {
    status = -1;
    df.warn('Loading failed', url, err);
  }
  return {
    data,
    status
  };
}
async function preload(target, url, format) {
  const df = this,
    pending = df._pending || loadPending(df);
  pending.requests += 1;
  const res = await df.request(url, format);
  df.pulse(target, df.changeset().remove(truthy).insert(res.data || []));
  pending.done();
  return res;
}
function loadPending(df) {
  let accept;
  const pending = new Promise(a => accept = a);
  pending.requests = 0;
  pending.done = () => {
    if (--pending.requests === 0) {
      df._pending = null;
      accept(df);
    }
  };
  return df._pending = pending;
}

const SKIP$2 = {
  skip: true
};

/**
 * Perform operator updates in response to events. Applies an
 * update function to compute a new operator value. If the update function
 * returns a {@link ChangeSet}, the operator will be pulsed with those tuple
 * changes. Otherwise, the operator value will be updated to the return value.
 * @param {EventStream|Operator} source - The event source to react to.
 *   This argument can be either an EventStream or an Operator.
 * @param {Operator|function(object):Operator} target - The operator to update.
 *   This argument can either be an Operator instance or (if the source
 *   argument is an EventStream), a function that accepts an event object as
 *   input and returns an Operator to target.
 * @param {function(Parameters,Event): *} [update] - Optional update function
 *   to compute the new operator value, or a literal value to set. Update
 *   functions expect to receive a parameter object and event as arguments.
 *   This function can either return a new operator value or (if the source
 *   argument is an EventStream) a {@link ChangeSet} instance to pulse
 *   the target operator with tuple changes.
 * @param {object} [params] - The update function parameters.
 * @param {object} [options] - Additional options hash. If not overridden,
 *   updated operators will be skipped by default.
 * @param {boolean} [options.skip] - If true, the operator will
 *  be skipped: it will not be evaluated, but its dependents will be.
 * @param {boolean} [options.force] - If true, the operator will
 *   be re-evaluated even if its value has not changed.
 * @return {Dataflow}
 */
function on (source, target, update, params, options) {
  const fn = source instanceof Operator ? onOperator : onStream;
  fn(this, source, target, update, params, options);
  return this;
}
function onStream(df, stream, target, update, params, options) {
  const opt = extend({}, options, SKIP$2);
  let func, op;
  if (!isFunction(target)) target = constant$1(target);
  if (update === undefined) {
    func = e => df.touch(target(e));
  } else if (isFunction(update)) {
    op = new Operator(null, update, params, false);
    func = e => {
      op.evaluate(e);
      const t = target(e),
        v = op.value;
      isChangeSet(v) ? df.pulse(t, v, options) : df.update(t, v, opt);
    };
  } else {
    func = e => df.update(target(e), update, opt);
  }
  stream.apply(func);
}
function onOperator(df, source, target, update, params, options) {
  if (update === undefined) {
    source.targets().add(target);
  } else {
    const opt = options || {},
      op = new Operator(null, updater(target, update), params, false);
    op.modified(opt.force);
    op.rank = source.rank; // immediately follow source
    source.targets().add(op); // add dependency

    if (target) {
      op.skip(true); // skip first invocation
      op.value = target.value; // initialize value
      op.targets().add(target); // chain dependencies
      df.connect(target, [op]); // rerank as needed, #1672
    }
  }
}
function updater(target, update) {
  update = isFunction(update) ? update : constant$1(update);
  return target ? function (_, pulse) {
    const value = update(_, pulse);
    if (!target.skip()) {
      target.skip(value !== this.value).value = value;
    }
    return value;
  } : update;
}

/**
 * Assigns a rank to an operator. Ranks are assigned in increasing order
 * by incrementing an internal rank counter.
 * @param {Operator} op - The operator to assign a rank.
 */
function rank(op) {
  op.rank = ++this._rank;
}

/**
 * Re-ranks an operator and all downstream target dependencies. This
 * is necessary when upstream dependencies of higher rank are added to
 * a target operator.
 * @param {Operator} op - The operator to re-rank.
 */
function rerank(op) {
  const queue = [op];
  let cur, list, i;
  while (queue.length) {
    this.rank(cur = queue.pop());
    if (list = cur._targets) {
      for (i = list.length; --i >= 0;) {
        queue.push(cur = list[i]);
        if (cur === op) error$1('Cycle detected in dataflow graph.');
      }
    }
  }
}

/**
 * Sentinel value indicating pulse propagation should stop.
 */
const StopPropagation = {};

// Pulse visit type flags
const ADD = 1 << 0,
  REM = 1 << 1,
  MOD$1 = 1 << 2,
  ADD_REM = ADD | REM,
  ADD_MOD = ADD | MOD$1,
  ALL = ADD | REM | MOD$1,
  REFLOW = 1 << 3,
  SOURCE = 1 << 4,
  NO_SOURCE = 1 << 5,
  NO_FIELDS = 1 << 6;

/**
 * A Pulse enables inter-operator communication during a run of the
 * dataflow graph. In addition to the current timestamp, a pulse may also
 * contain a change-set of added, removed or modified data tuples, as well as
 * a pointer to a full backing data source. Tuple change sets may not
 * be fully materialized; for example, to prevent needless array creation
 * a change set may include larger arrays and corresponding filter functions.
 * The pulse provides a {@link visit} method to enable proper and efficient
 * iteration over requested data tuples.
 *
 * In addition, each pulse can track modification flags for data tuple fields.
 * Responsible transform operators should call the {@link modifies} method to
 * indicate changes to data fields. The {@link modified} method enables
 * querying of this modification state.
 *
 * @constructor
 * @param {Dataflow} dataflow - The backing dataflow instance.
 * @param {number} stamp - The current propagation timestamp.
 * @param {string} [encode] - An optional encoding set name, which is then
 *   accessible as Pulse.encode. Operators can respond to (or ignore) this
 *   setting as appropriate. This parameter can be used in conjunction with
 *   the Encode transform in the vega-encode module.
 */
function Pulse(dataflow, stamp, encode) {
  this.dataflow = dataflow;
  this.stamp = stamp == null ? -1 : stamp;
  this.add = [];
  this.rem = [];
  this.mod = [];
  this.fields = null;
  this.encode = encode || null;
}
function materialize(data, filter) {
  const out = [];
  visitArray(data, filter, _ => out.push(_));
  return out;
}
function filter$1(pulse, flags) {
  const map = {};
  pulse.visit(flags, t => {
    map[tupleid(t)] = 1;
  });
  return t => map[tupleid(t)] ? null : t;
}
function addFilter(a, b) {
  return a ? (t, i) => a(t, i) && b(t, i) : b;
}
Pulse.prototype = {
  /**
   * Sentinel value indicating pulse propagation should stop.
   */
  StopPropagation,
  /**
   * Boolean flag indicating ADD (added) tuples.
   */
  ADD,
  /**
   * Boolean flag indicating REM (removed) tuples.
   */
  REM,
  /**
   * Boolean flag indicating MOD (modified) tuples.
   */
  MOD: MOD$1,
  /**
   * Boolean flag indicating ADD (added) and REM (removed) tuples.
   */
  ADD_REM,
  /**
   * Boolean flag indicating ADD (added) and MOD (modified) tuples.
   */
  ADD_MOD,
  /**
   * Boolean flag indicating ADD, REM and MOD tuples.
   */
  ALL,
  /**
   * Boolean flag indicating all tuples in a data source
   * except for the ADD, REM and MOD tuples.
   */
  REFLOW,
  /**
   * Boolean flag indicating a 'pass-through' to a
   * backing data source, ignoring ADD, REM and MOD tuples.
   */
  SOURCE,
  /**
   * Boolean flag indicating that source data should be
   * suppressed when creating a forked pulse.
   */
  NO_SOURCE,
  /**
   * Boolean flag indicating that field modifications should be
   * suppressed when creating a forked pulse.
   */
  NO_FIELDS,
  /**
   * Creates a new pulse based on the values of this pulse.
   * The dataflow, time stamp and field modification values are copied over.
   * By default, new empty ADD, REM and MOD arrays are created.
   * @param {number} flags - Integer of boolean flags indicating which (if any)
   *   tuple arrays should be copied to the new pulse. The supported flag values
   *   are ADD, REM and MOD. Array references are copied directly: new array
   *   instances are not created.
   * @return {Pulse} - The forked pulse instance.
   * @see init
   */
  fork(flags) {
    return new Pulse(this.dataflow).init(this, flags);
  },
  /**
   * Creates a copy of this pulse with new materialized array
   * instances for the ADD, REM, MOD, and SOURCE arrays.
   * The dataflow, time stamp and field modification values are copied over.
   * @return {Pulse} - The cloned pulse instance.
   * @see init
   */
  clone() {
    const p = this.fork(ALL);
    p.add = p.add.slice();
    p.rem = p.rem.slice();
    p.mod = p.mod.slice();
    if (p.source) p.source = p.source.slice();
    return p.materialize(ALL | SOURCE);
  },
  /**
   * Returns a pulse that adds all tuples from a backing source. This is
   * useful for cases where operators are added to a dataflow after an
   * upstream data pipeline has already been processed, ensuring that
   * new operators can observe all tuples within a stream.
   * @return {Pulse} - A pulse instance with all source tuples included
   *   in the add array. If the current pulse already has all source
   *   tuples in its add array, it is returned directly. If the current
   *   pulse does not have a backing source, it is returned directly.
   */
  addAll() {
    let p = this;
    const reuse = !p.source || p.add === p.rem // special case for indexed set (e.g., crossfilter)
    || !p.rem.length && p.source.length === p.add.length;
    if (reuse) {
      return p;
    } else {
      p = new Pulse(this.dataflow).init(this);
      p.add = p.source;
      p.rem = []; // new operators can ignore rem #2769
      return p;
    }
  },
  /**
   * Initialize this pulse based on the values of another pulse. This method
   * is used internally by {@link fork} to initialize a new forked tuple.
   * The dataflow, time stamp and field modification values are copied over.
   * By default, new empty ADD, REM and MOD arrays are created.
   * @param {Pulse} src - The source pulse to copy from.
   * @param {number} flags - Integer of boolean flags indicating which (if any)
   *   tuple arrays should be copied to the new pulse. The supported flag values
   *   are ADD, REM and MOD. Array references are copied directly: new array
   *   instances are not created. By default, source data arrays are copied
   *   to the new pulse. Use the NO_SOURCE flag to enforce a null source.
   * @return {Pulse} - Returns this Pulse instance.
   */
  init(src, flags) {
    const p = this;
    p.stamp = src.stamp;
    p.encode = src.encode;
    if (src.fields && !(flags & NO_FIELDS)) {
      p.fields = src.fields;
    }
    if (flags & ADD) {
      p.addF = src.addF;
      p.add = src.add;
    } else {
      p.addF = null;
      p.add = [];
    }
    if (flags & REM) {
      p.remF = src.remF;
      p.rem = src.rem;
    } else {
      p.remF = null;
      p.rem = [];
    }
    if (flags & MOD$1) {
      p.modF = src.modF;
      p.mod = src.mod;
    } else {
      p.modF = null;
      p.mod = [];
    }
    if (flags & NO_SOURCE) {
      p.srcF = null;
      p.source = null;
    } else {
      p.srcF = src.srcF;
      p.source = src.source;
      if (src.cleans) p.cleans = src.cleans;
    }
    return p;
  },
  /**
   * Schedules a function to run after pulse propagation completes.
   * @param {function} func - The function to run.
   */
  runAfter(func) {
    this.dataflow.runAfter(func);
  },
  /**
   * Indicates if tuples have been added, removed or modified.
   * @param {number} [flags] - The tuple types (ADD, REM or MOD) to query.
   *   Defaults to ALL, returning true if any tuple type has changed.
   * @return {boolean} - Returns true if one or more queried tuple types have
   *   changed, false otherwise.
   */
  changed(flags) {
    const f = flags || ALL;
    return f & ADD && this.add.length || f & REM && this.rem.length || f & MOD$1 && this.mod.length;
  },
  /**
   * Forces a "reflow" of tuple values, such that all tuples in the backing
   * source are added to the MOD set, unless already present in the ADD set.
   * @param {boolean} [fork=false] - If true, returns a forked copy of this
   *   pulse, and invokes reflow on that derived pulse.
   * @return {Pulse} - The reflowed pulse instance.
   */
  reflow(fork) {
    if (fork) return this.fork(ALL).reflow();
    const len = this.add.length,
      src = this.source && this.source.length;
    if (src && src !== len) {
      this.mod = this.source;
      if (len) this.filter(MOD$1, filter$1(this, ADD));
    }
    return this;
  },
  /**
   * Get/set metadata to pulse requesting garbage collection
   * to reclaim currently unused resources.
   */
  clean(value) {
    if (arguments.length) {
      this.cleans = !!value;
      return this;
    } else {
      return this.cleans;
    }
  },
  /**
   * Marks one or more data field names as modified to assist dependency
   * tracking and incremental processing by transform operators.
   * @param {string|Array<string>} _ - The field(s) to mark as modified.
   * @return {Pulse} - This pulse instance.
   */
  modifies(_) {
    const hash = this.fields || (this.fields = {});
    if (isArray(_)) {
      _.forEach(f => hash[f] = true);
    } else {
      hash[_] = true;
    }
    return this;
  },
  /**
   * Checks if one or more data fields have been modified during this pulse
   * propagation timestamp.
   * @param {string|Array<string>} _ - The field(s) to check for modified.
   * @param {boolean} nomod - If true, will check the modified flag even if
   *   no mod tuples exist. If false (default), mod tuples must be present.
   * @return {boolean} - Returns true if any of the provided fields has been
   *   marked as modified, false otherwise.
   */
  modified(_, nomod) {
    const fields = this.fields;
    return !((nomod || this.mod.length) && fields) ? false : !arguments.length ? !!fields : isArray(_) ? _.some(f => fields[f]) : fields[_];
  },
  /**
   * Adds a filter function to one more tuple sets. Filters are applied to
   * backing tuple arrays, to determine the actual set of tuples considered
   * added, removed or modified. They can be used to delay materialization of
   * a tuple set in order to avoid expensive array copies. In addition, the
   * filter functions can serve as value transformers: unlike standard predicate
   * function (which return boolean values), Pulse filters should return the
   * actual tuple value to process. If a tuple set is already filtered, the
   * new filter function will be appended into a conjuntive ('and') query.
   * @param {number} flags - Flags indicating the tuple set(s) to filter.
   * @param {function(*):object} filter - Filter function that will be applied
   *   to the tuple set array, and should return a data tuple if the value
   *   should be included in the tuple set, and falsy (or null) otherwise.
   * @return {Pulse} - Returns this pulse instance.
   */
  filter(flags, filter) {
    const p = this;
    if (flags & ADD) p.addF = addFilter(p.addF, filter);
    if (flags & REM) p.remF = addFilter(p.remF, filter);
    if (flags & MOD$1) p.modF = addFilter(p.modF, filter);
    if (flags & SOURCE) p.srcF = addFilter(p.srcF, filter);
    return p;
  },
  /**
   * Materialize one or more tuple sets in this pulse. If the tuple set(s) have
   * a registered filter function, it will be applied and the tuple set(s) will
   * be replaced with materialized tuple arrays.
   * @param {number} flags - Flags indicating the tuple set(s) to materialize.
   * @return {Pulse} - Returns this pulse instance.
   */
  materialize(flags) {
    flags = flags || ALL;
    const p = this;
    if (flags & ADD && p.addF) {
      p.add = materialize(p.add, p.addF);
      p.addF = null;
    }
    if (flags & REM && p.remF) {
      p.rem = materialize(p.rem, p.remF);
      p.remF = null;
    }
    if (flags & MOD$1 && p.modF) {
      p.mod = materialize(p.mod, p.modF);
      p.modF = null;
    }
    if (flags & SOURCE && p.srcF) {
      p.source = p.source.filter(p.srcF);
      p.srcF = null;
    }
    return p;
  },
  /**
   * Visit one or more tuple sets in this pulse.
   * @param {number} flags - Flags indicating the tuple set(s) to visit.
   *   Legal values are ADD, REM, MOD and SOURCE (if a backing data source
   *   has been set).
   * @param {function(object):*} - Visitor function invoked per-tuple.
   * @return {Pulse} - Returns this pulse instance.
   */
  visit(flags, visitor) {
    const p = this,
      v = visitor;
    if (flags & SOURCE) {
      visitArray(p.source, p.srcF, v);
      return p;
    }
    if (flags & ADD) visitArray(p.add, p.addF, v);
    if (flags & REM) visitArray(p.rem, p.remF, v);
    if (flags & MOD$1) visitArray(p.mod, p.modF, v);
    const src = p.source;
    if (flags & REFLOW && src) {
      const sum = p.add.length + p.mod.length;
      if (sum === src.length) ; else if (sum) {
        visitArray(src, filter$1(p, ADD_MOD), v);
      } else {
        // if no add/rem/mod tuples, visit source
        visitArray(src, p.srcF, v);
      }
    }
    return p;
  }
};

/**
 * Represents a set of multiple pulses. Used as input for operators
 * that accept multiple pulses at a time. Contained pulses are
 * accessible via the public "pulses" array property. This pulse doe
 * not carry added, removed or modified tuples directly. However,
 * the visit method can be used to traverse all such tuples contained
 * in sub-pulses with a timestamp matching this parent multi-pulse.
 * @constructor
 * @param {Dataflow} dataflow - The backing dataflow instance.
 * @param {number} stamp - The timestamp.
 * @param {Array<Pulse>} pulses - The sub-pulses for this multi-pulse.
 */
function MultiPulse(dataflow, stamp, pulses, encode) {
  const p = this;
  let c = 0;
  this.dataflow = dataflow;
  this.stamp = stamp;
  this.fields = null;
  this.encode = encode || null;
  this.pulses = pulses;
  for (const pulse of pulses) {
    if (pulse.stamp !== stamp) continue;
    if (pulse.fields) {
      const hash = p.fields || (p.fields = {});
      for (const f in pulse.fields) {
        hash[f] = 1;
      }
    }
    if (pulse.changed(p.ADD)) c |= p.ADD;
    if (pulse.changed(p.REM)) c |= p.REM;
    if (pulse.changed(p.MOD)) c |= p.MOD;
  }
  this.changes = c;
}
inherits(MultiPulse, Pulse, {
  /**
   * Creates a new pulse based on the values of this pulse.
   * The dataflow, time stamp and field modification values are copied over.
   * @return {Pulse}
   */
  fork(flags) {
    const p = new Pulse(this.dataflow).init(this, flags & this.NO_FIELDS);
    if (flags !== undefined) {
      if (flags & p.ADD) this.visit(p.ADD, t => p.add.push(t));
      if (flags & p.REM) this.visit(p.REM, t => p.rem.push(t));
      if (flags & p.MOD) this.visit(p.MOD, t => p.mod.push(t));
    }
    return p;
  },
  changed(flags) {
    return this.changes & flags;
  },
  modified(_) {
    const p = this,
      fields = p.fields;
    return !(fields && p.changes & p.MOD) ? 0 : isArray(_) ? _.some(f => fields[f]) : fields[_];
  },
  filter() {
    error$1('MultiPulse does not support filtering.');
  },
  materialize() {
    error$1('MultiPulse does not support materialization.');
  },
  visit(flags, visitor) {
    const p = this,
      pulses = p.pulses,
      n = pulses.length;
    let i = 0;
    if (flags & p.SOURCE) {
      for (; i < n; ++i) {
        pulses[i].visit(flags, visitor);
      }
    } else {
      for (; i < n; ++i) {
        if (pulses[i].stamp === p.stamp) {
          pulses[i].visit(flags, visitor);
        }
      }
    }
    return p;
  }
});

/**
 * Evaluates the dataflow and returns a Promise that resolves when pulse
 * propagation completes. This method will increment the current timestamp
 * and process all updated, pulsed and touched operators. When invoked for
 * the first time, all registered operators will be processed. This method
 * should not be invoked by third-party clients, use {@link runAsync} or
 * {@link run} instead.
 * @param {string} [encode] - The name of an encoding set to invoke during
 *   propagation. This value is added to generated Pulse instances;
 *   operators can then respond to (or ignore) this setting as appropriate.
 *   This parameter can be used in conjunction with the Encode transform in
 *   the vega-encode package.
 * @param {function} [prerun] - An optional callback function to invoke
 *   immediately before dataflow evaluation commences.
 * @param {function} [postrun] - An optional callback function to invoke
 *   after dataflow evaluation completes. The callback will be invoked
 *   after those registered via {@link runAfter}.
 * @return {Promise} - A promise that resolves to this dataflow after
 *   evaluation completes.
 */
async function evaluate(encode, prerun, postrun) {
  const df = this,
    async = [];

  // if the pulse value is set, this is a re-entrant call
  if (df._pulse) return reentrant(df);

  // wait for pending datasets to load
  if (df._pending) await df._pending;

  // invoke prerun function, if provided
  if (prerun) await asyncCallback(df, prerun);

  // exit early if there are no updates
  if (!df._touched.length) {
    df.debug('Dataflow invoked, but nothing to do.');
    return df;
  }

  // increment timestamp clock
  const stamp = ++df._clock;

  // set the current pulse
  df._pulse = new Pulse(df, stamp, encode);

  // initialize priority queue, reset touched operators
  df._touched.forEach(op => df._enqueue(op, true));
  df._touched = UniqueList(id);
  let count = 0,
    op,
    next,
    error;
  try {
    while (df._heap.size() > 0) {
      // dequeue operator with highest priority
      op = df._heap.pop();

      // re-queue if rank changed
      if (op.rank !== op.qrank) {
        df._enqueue(op, true);
        continue;
      }

      // otherwise, evaluate the operator
      next = op.run(df._getPulse(op, encode));
      if (next.then) {
        // await if operator returns a promise directly
        next = await next;
      } else if (next.async) {
        // queue parallel asynchronous execution
        async.push(next.async);
        next = StopPropagation;
      }

      // propagate evaluation, enqueue dependent operators
      if (next !== StopPropagation) {
        if (op._targets) op._targets.forEach(op => df._enqueue(op));
      }

      // increment visit counter
      ++count;
    }
  } catch (err) {
    df._heap.clear();
    error = err;
  }

  // reset pulse map
  df._input = {};
  df._pulse = null;
  df.debug(`Pulse ${stamp}: ${count} operators`);
  if (error) {
    df._postrun = [];
    df.error(error);
  }

  // invoke callbacks queued via runAfter
  if (df._postrun.length) {
    const pr = df._postrun.sort((a, b) => b.priority - a.priority);
    df._postrun = [];
    for (let i = 0; i < pr.length; ++i) {
      await asyncCallback(df, pr[i].callback);
    }
  }

  // invoke postrun function, if provided
  if (postrun) await asyncCallback(df, postrun);

  // handle non-blocking asynchronous callbacks
  if (async.length) {
    Promise.all(async).then(cb => df.runAsync(null, () => {
      cb.forEach(f => {
        try {
          f(df);
        } catch (err) {
          df.error(err);
        }
      });
    }));
  }
  return df;
}

/**
 * Queues dataflow evaluation to run once any other queued evaluations have
 * completed and returns a Promise that resolves when the queued pulse
 * propagation completes. If provided, a callback function will be invoked
 * immediately before evaluation commences. This method will ensure a
 * separate evaluation is invoked for each time it is called.
 * @param {string} [encode] - The name of an encoding set to invoke during
 *   propagation. This value is added to generated Pulse instances;
 *   operators can then respond to (or ignore) this setting as appropriate.
 *   This parameter can be used in conjunction with the Encode transform in
 *   the vega-encode package.
 * @param {function} [prerun] - An optional callback function to invoke
 *   immediately before dataflow evaluation commences.
 * @param {function} [postrun] - An optional callback function to invoke
 *   after dataflow evaluation completes. The callback will be invoked
 *   after those registered via {@link runAfter}.
 * @return {Promise} - A promise that resolves to this dataflow after
 *   evaluation completes.
 */
async function runAsync(encode, prerun, postrun) {
  // await previously queued functions
  while (this._running) await this._running;

  // run dataflow, manage running promise
  const clear = () => this._running = null;
  (this._running = this.evaluate(encode, prerun, postrun)).then(clear, clear);
  return this._running;
}

/**
 * Requests dataflow evaluation and the immediately returns this dataflow
 * instance. If there are pending data loading or other asynchronous
 * operations, the dataflow will evaluate asynchronously after this method
 * has been invoked. To track when dataflow evaluation completes, use the
 * {@link runAsync} method instead. This method will raise an error if
 * invoked while the dataflow is already in the midst of evaluation.
 * @param {string} [encode] - The name of an encoding set to invoke during
 *   propagation. This value is added to generated Pulse instances;
 *   operators can then respond to (or ignore) this setting as appropriate.
 *   This parameter can be used in conjunction with the Encode transform in
 *   the vega-encode module.
 * @param {function} [prerun] - An optional callback function to invoke
 *   immediately before dataflow evaluation commences.
 * @param {function} [postrun] - An optional callback function to invoke
 *   after dataflow evaluation completes. The callback will be invoked
 *   after those registered via {@link runAfter}.
 * @return {Dataflow} - This dataflow instance.
 */
function run(encode, prerun, postrun) {
  return this._pulse ? reentrant(this) : (this.evaluate(encode, prerun, postrun), this);
}

/**
 * Schedules a callback function to be invoked after the current pulse
 * propagation completes. If no propagation is currently occurring,
 * the function is invoked immediately. Callbacks scheduled via runAfter
 * are invoked immediately upon completion of the current cycle, before
 * any request queued via runAsync. This method is primarily intended for
 * internal use. Third-party callers using runAfter to schedule a callback
 * that invokes {@link run} or {@link runAsync} should not use this method,
 * but instead use {@link runAsync} with prerun or postrun arguments.
 * @param {function(Dataflow)} callback - The callback function to run.
 *   The callback will be invoked with this Dataflow instance as its
 *   sole argument.
 * @param {boolean} enqueue - A boolean flag indicating that the
 *   callback should be queued up to run after the next propagation
 *   cycle, suppressing immediate invocation when propagation is not
 *   currently occurring.
 * @param {number} [priority] - A priority value used to sort registered
 *   callbacks to determine execution order. This argument is intended
 *   for internal Vega use only.
 */
function runAfter(callback, enqueue, priority) {
  if (this._pulse || enqueue) {
    // pulse propagation is currently running, queue to run after
    this._postrun.push({
      priority: priority || 0,
      callback: callback
    });
  } else {
    // pulse propagation already complete, invoke immediately
    try {
      callback(this);
    } catch (err) {
      this.error(err);
    }
  }
}

/**
 * Raise an error for re-entrant dataflow evaluation.
 */
function reentrant(df) {
  df.error('Dataflow already running. Use runAsync() to chain invocations.');
  return df;
}

/**
 * Enqueue an operator into the priority queue for evaluation. The operator
 * will be enqueued if it has no registered pulse for the current cycle, or if
 * the force argument is true. Upon enqueue, this method also sets the
 * operator's qrank to the current rank value.
 * @param {Operator} op - The operator to enqueue.
 * @param {boolean} [force] - A flag indicating if the operator should be
 *   forceably added to the queue, even if it has already been previously
 *   enqueued during the current pulse propagation. This is useful when the
 *   dataflow graph is dynamically modified and the operator rank changes.
 */
function enqueue(op, force) {
  const q = op.stamp < this._clock;
  if (q) op.stamp = this._clock;
  if (q || force) {
    op.qrank = op.rank;
    this._heap.push(op);
  }
}

/**
 * Provide a correct pulse for evaluating an operator. If the operator has an
 * explicit source operator, we will try to pull the pulse(s) from it.
 * If there is an array of source operators, we build a multi-pulse.
 * Otherwise, we return a current pulse with correct source data.
 * If the pulse is the pulse map has an explicit target set, we use that.
 * Else if the pulse on the upstream source operator is current, we use that.
 * Else we use the pulse from the pulse map, but copy the source tuple array.
 * @param {Operator} op - The operator for which to get an input pulse.
 * @param {string} [encode] - An (optional) encoding set name with which to
 *   annotate the returned pulse. See {@link run} for more information.
 */
function getPulse(op, encode) {
  const s = op.source,
    stamp = this._clock;
  return s && isArray(s) ? new MultiPulse(this, stamp, s.map(_ => _.pulse), encode) : this._input[op.id] || singlePulse(this._pulse, s && s.pulse);
}
function singlePulse(p, s) {
  if (s && s.stamp === p.stamp) {
    return s;
  }
  p = p.fork();
  if (s && s !== StopPropagation) {
    p.source = s.source;
  }
  return p;
}

const NO_OPT = {
  skip: false,
  force: false
};

/**
 * Touches an operator, scheduling it to be evaluated. If invoked outside of
 * a pulse propagation, the operator will be evaluated the next time this
 * dataflow is run. If invoked in the midst of pulse propagation, the operator
 * will be queued for evaluation if and only if the operator has not yet been
 * evaluated on the current propagation timestamp.
 * @param {Operator} op - The operator to touch.
 * @param {object} [options] - Additional options hash.
 * @param {boolean} [options.skip] - If true, the operator will
 *   be skipped: it will not be evaluated, but its dependents will be.
 * @return {Dataflow}
 */
function touch(op, options) {
  const opt = options || NO_OPT;
  if (this._pulse) {
    // if in midst of propagation, add to priority queue
    this._enqueue(op);
  } else {
    // otherwise, queue for next propagation
    this._touched.add(op);
  }
  if (opt.skip) op.skip(true);
  return this;
}

/**
 * Updates the value of the given operator.
 * @param {Operator} op - The operator to update.
 * @param {*} value - The value to set.
 * @param {object} [options] - Additional options hash.
 * @param {boolean} [options.force] - If true, the operator will
 *   be re-evaluated even if its value has not changed.
 * @param {boolean} [options.skip] - If true, the operator will
 *   be skipped: it will not be evaluated, but its dependents will be.
 * @return {Dataflow}
 */
function update$6(op, value, options) {
  const opt = options || NO_OPT;
  if (op.set(value) || opt.force) {
    this.touch(op, opt);
  }
  return this;
}

/**
 * Pulses an operator with a changeset of tuples. If invoked outside of
 * a pulse propagation, the pulse will be applied the next time this
 * dataflow is run. If invoked in the midst of pulse propagation, the pulse
 * will be added to the set of active pulses and will be applied if and
 * only if the target operator has not yet been evaluated on the current
 * propagation timestamp.
 * @param {Operator} op - The operator to pulse.
 * @param {ChangeSet} value - The tuple changeset to apply.
 * @param {object} [options] - Additional options hash.
 * @param {boolean} [options.skip] - If true, the operator will
 *   be skipped: it will not be evaluated, but its dependents will be.
 * @return {Dataflow}
 */
function pulse(op, changeset, options) {
  this.touch(op, options || NO_OPT);
  const p = new Pulse(this, this._clock + (this._pulse ? 0 : 1)),
    t = op.pulse && op.pulse.source || [];
  p.target = op;
  this._input[op.id] = changeset.pulse(p, t);
  return this;
}

function Heap(cmp) {
  let nodes = [];
  return {
    clear: () => nodes = [],
    size: () => nodes.length,
    peek: () => nodes[0],
    push: x => {
      nodes.push(x);
      return siftdown(nodes, 0, nodes.length - 1, cmp);
    },
    pop: () => {
      const last = nodes.pop();
      let item;
      if (nodes.length) {
        item = nodes[0];
        nodes[0] = last;
        siftup(nodes, 0, cmp);
      } else {
        item = last;
      }
      return item;
    }
  };
}
function siftdown(array, start, idx, cmp) {
  let parent, pidx;
  const item = array[idx];
  while (idx > start) {
    pidx = idx - 1 >> 1;
    parent = array[pidx];
    if (cmp(item, parent) < 0) {
      array[idx] = parent;
      idx = pidx;
      continue;
    }
    break;
  }
  return array[idx] = item;
}
function siftup(array, idx, cmp) {
  const start = idx,
    end = array.length,
    item = array[idx];
  let cidx = (idx << 1) + 1,
    ridx;
  while (cidx < end) {
    ridx = cidx + 1;
    if (ridx < end && cmp(array[cidx], array[ridx]) >= 0) {
      cidx = ridx;
    }
    array[idx] = array[cidx];
    idx = cidx;
    cidx = (idx << 1) + 1;
  }
  array[idx] = item;
  return siftdown(array, start, idx, cmp);
}

/**
 * A dataflow graph for reactive processing of data streams.
 * @constructor
 */
function Dataflow() {
  this.logger(logger());
  this.logLevel(Error$1);
  this._clock = 0;
  this._rank = 0;
  this._locale = defaultLocale();
  try {
    this._loader = loader();
  } catch (e) {
    // do nothing if loader module is unavailable
  }
  this._touched = UniqueList(id);
  this._input = {};
  this._pulse = null;
  this._heap = Heap((a, b) => a.qrank - b.qrank);
  this._postrun = [];
}
function logMethod(method) {
  return function () {
    return this._log[method].apply(this, arguments);
  };
}
Dataflow.prototype = {
  /**
   * The current timestamp of this dataflow. This value reflects the
   * timestamp of the previous dataflow run. The dataflow is initialized
   * with a stamp value of 0. The initial run of the dataflow will have
   * a timestap of 1, and so on. This value will match the
   * {@link Pulse.stamp} property.
   * @return {number} - The current timestamp value.
   */
  stamp() {
    return this._clock;
  },
  /**
   * Gets or sets the loader instance to use for data file loading. A
   * loader object must provide a "load" method for loading files and a
   * "sanitize" method for checking URL/filename validity. Both methods
   * should accept a URI and options hash as arguments, and return a Promise
   * that resolves to the loaded file contents (load) or a hash containing
   * sanitized URI data with the sanitized url assigned to the "href" property
   * (sanitize).
   * @param {object} _ - The loader instance to use.
   * @return {object|Dataflow} - If no arguments are provided, returns
   *   the current loader instance. Otherwise returns this Dataflow instance.
   */
  loader(_) {
    if (arguments.length) {
      this._loader = _;
      return this;
    } else {
      return this._loader;
    }
  },
  /**
   * Gets or sets the locale instance to use for formatting and parsing
   * string values. The locale object should be provided by the
   * vega-format library, and include methods such as format, timeFormat,
   * utcFormat, timeParse, and utcParse.
   * @param {object} _ - The locale instance to use.
   * @return {object|Dataflow} - If no arguments are provided, returns
   *   the current locale instance. Otherwise returns this Dataflow instance.
   */
  locale(_) {
    if (arguments.length) {
      this._locale = _;
      return this;
    } else {
      return this._locale;
    }
  },
  /**
   * Get or set the logger instance used to log messages. If no arguments are
   * provided, returns the current logger instance. Otherwise, sets the logger
   * and return this Dataflow instance. Provided loggers must support the full
   * API of logger objects generated by the vega-util logger method. Note that
   * by default the log level of the new logger will be used; use the logLevel
   * method to adjust the log level as needed.
   */
  logger(logger) {
    if (arguments.length) {
      this._log = logger;
      return this;
    } else {
      return this._log;
    }
  },
  /**
   * Logs an error message. By default, logged messages are written to console
   * output. The message will only be logged if the current log level is high
   * enough to permit error messages.
   */
  error: logMethod('error'),
  /**
   * Logs a warning message. By default, logged messages are written to console
   * output. The message will only be logged if the current log level is high
   * enough to permit warning messages.
   */
  warn: logMethod('warn'),
  /**
   * Logs a information message. By default, logged messages are written to
   * console output. The message will only be logged if the current log level is
   * high enough to permit information messages.
   */
  info: logMethod('info'),
  /**
   * Logs a debug message. By default, logged messages are written to console
   * output. The message will only be logged if the current log level is high
   * enough to permit debug messages.
   */
  debug: logMethod('debug'),
  /**
   * Get or set the current log level. If an argument is provided, it
   * will be used as the new log level.
   * @param {number} [level] - Should be one of None, Warn, Info
   * @return {number} - The current log level.
   */
  logLevel: logMethod('level'),
  /**
   * Empty entry threshold for garbage cleaning. Map data structures will
   * perform cleaning once the number of empty entries exceeds this value.
   */
  cleanThreshold: 1e4,
  // OPERATOR REGISTRATION
  add: add$3,
  connect,
  rank,
  rerank,
  // OPERATOR UPDATES
  pulse,
  touch,
  update: update$6,
  changeset,
  // DATA LOADING
  ingest,
  parse: parse$5,
  preload,
  request,
  // EVENT HANDLING
  events: events$1,
  on,
  // PULSE PROPAGATION
  evaluate,
  run,
  runAsync,
  runAfter,
  _enqueue: enqueue,
  _getPulse: getPulse
};

/**
 * Abstract class for operators that process data tuples.
 * Subclasses must provide a {@link transform} method for operator processing.
 * @constructor
 * @param {*} [init] - The initial value for this operator.
 * @param {object} [params] - The parameters for this operator.
 * @param {Operator} [source] - The operator from which to receive pulses.
 */
function Transform(init, params) {
  Operator.call(this, init, null, params);
}
inherits(Transform, Operator, {
  /**
   * Overrides {@link Operator.evaluate} for transform operators.
   * Internally, this method calls {@link evaluate} to perform processing.
   * If {@link evaluate} returns a falsy value, the input pulse is returned.
   * This method should NOT be overridden, instead overrride {@link evaluate}.
   * @param {Pulse} pulse - the current dataflow pulse.
   * @return the output pulse for this operator (or StopPropagation)
   */
  run(pulse) {
    if (pulse.stamp < this.stamp) return pulse.StopPropagation;
    let rv;
    if (this.skip()) {
      this.skip(false);
    } else {
      rv = this.evaluate(pulse);
    }
    rv = rv || pulse;
    if (rv.then) {
      rv = rv.then(_ => this.pulse = _);
    } else if (rv !== pulse.StopPropagation) {
      this.pulse = rv;
    }
    return rv;
  },
  /**
   * Overrides {@link Operator.evaluate} for transform operators.
   * Marshalls parameter values and then invokes {@link transform}.
   * @param {Pulse} pulse - the current dataflow pulse.
   * @return {Pulse} The output pulse (or StopPropagation). A falsy return
       value (including undefined) will let the input pulse pass through.
  */
  evaluate(pulse) {
    const params = this.marshall(pulse.stamp),
      out = this.transform(params, pulse);
    params.clear();
    return out;
  },
  /**
   * Process incoming pulses.
   * Subclasses should override this method to implement transforms.
   * @param {Parameters} _ - The operator parameter values.
   * @param {Pulse} pulse - The current dataflow pulse.
   * @return {Pulse} The output pulse (or StopPropagation). A falsy return
   *   value (including undefined) will let the input pulse pass through.
   */
  transform() {}
});

const transforms = {};
function definition$1(type) {
  const t = transform$2(type);
  return t && t.Definition || null;
}
function transform$2(type) {
  type = type && type.toLowerCase();
  return has$1(transforms, type) ? transforms[type] : null;
}

function* numbers$1 (values, valueof) {
  if (valueof == null) {
    for (let value of values) {
      if (value != null && value !== '' && (value = +value) >= value) {
        yield value;
      }
    }
  } else {
    let index = -1;
    for (let value of values) {
      value = valueof(value, ++index, values);
      if (value != null && value !== '' && (value = +value) >= value) {
        yield value;
      }
    }
  }
}

function quantiles (array, p, f) {
  const values = Float64Array.from(numbers$1(array, f));

  // don't depend on return value from typed array sort call
  // protects against undefined sort results in Safari (vega/vega-lite#4964)
  values.sort(ascending$2);
  return p.map(_ => quantileSorted(values, _));
}

function quartiles (array, f) {
  return quantiles(array, [0.25, 0.50, 0.75], f);
}

// Scott, D. W. (1992) Multivariate Density Estimation:
// Theory, Practice, and Visualization. Wiley.
function estimateBandwidth (array, f) {
  const n = array.length,
    d = deviation(array, f),
    q = quartiles(array, f),
    h = (q[2] - q[0]) / 1.34,
    v = Math.min(d, h) || d || Math.abs(q[0]) || 1;
  return 1.06 * v * Math.pow(n, -0.2);
}

function bin (_) {
  // determine range
  const maxb = _.maxbins || 20,
    base = _.base || 10,
    logb = Math.log(base),
    div = _.divide || [5, 2];
  let min = _.extent[0],
    max = _.extent[1],
    step,
    level,
    minstep,
    v,
    i,
    n;
  const span = _.span || max - min || Math.abs(min) || 1;
  if (_.step) {
    // if step size is explicitly given, use that
    step = _.step;
  } else if (_.steps) {
    // if provided, limit choice to acceptable step sizes
    v = span / maxb;
    for (i = 0, n = _.steps.length; i < n && _.steps[i] < v; ++i);
    step = _.steps[Math.max(0, i - 1)];
  } else {
    // else use span to determine step size
    level = Math.ceil(Math.log(maxb) / logb);
    minstep = _.minstep || 0;
    step = Math.max(minstep, Math.pow(base, Math.round(Math.log(span) / logb) - level));

    // increase step size if too many bins
    while (Math.ceil(span / step) > maxb) {
      step *= base;
    }

    // decrease step size if allowed
    for (i = 0, n = div.length; i < n; ++i) {
      v = step / div[i];
      if (v >= minstep && span / v <= maxb) step = v;
    }
  }

  // update precision, min and max
  v = Math.log(step);
  const precision = v >= 0 ? 0 : ~~(-v / logb) + 1,
    eps = Math.pow(base, -precision - 1);
  if (_.nice || _.nice === undefined) {
    v = Math.floor(min / step + eps) * step;
    min = min < v ? v - step : v;
    max = Math.ceil(max / step) * step;
  }
  return {
    start: min,
    stop: max === min ? min + step : max,
    step: step
  };
}

var random = Math.random;
function setRandom(r) {
  random = r;
}

function bootstrapCI (array, samples, alpha, f) {
  if (!array.length) return [undefined, undefined];
  const values = Float64Array.from(numbers$1(array, f)),
    n = values.length,
    m = samples;
  let a, i, j, mu;
  for (j = 0, mu = Array(m); j < m; ++j) {
    for (a = 0, i = 0; i < n; ++i) {
      a += values[~~(random() * n)];
    }
    mu[j] = a / n;
  }
  mu.sort(ascending$2);
  return [quantile(mu, alpha / 2), quantile(mu, 1 - alpha / 2)];
}

// Dot density binning for dot plot construction.
// Based on Leland Wilkinson, Dot Plots, The American Statistician, 1999.
// https://www.cs.uic.edu/~wilkinson/Publications/dotplots.pdf
function dotbin (array, step, smooth, f) {
  f = f || (_ => _);
  const n = array.length,
    v = new Float64Array(n);
  let i = 0,
    j = 1,
    a = f(array[0]),
    b = a,
    w = a + step,
    x;
  for (; j < n; ++j) {
    x = f(array[j]);
    if (x >= w) {
      b = (a + b) / 2;
      for (; i < j; ++i) v[i] = b;
      w = x + step;
      a = x;
    }
    b = x;
  }
  b = (a + b) / 2;
  for (; i < j; ++i) v[i] = b;
  return smooth ? smoothing(v, step + step / 4) : v;
}

// perform smoothing to reduce variance
// swap points between "adjacent" stacks
// Wilkinson defines adjacent as within step/4 units
function smoothing(v, thresh) {
  const n = v.length;
  let a = 0,
    b = 1,
    c,
    d;

  // get left stack
  while (v[a] === v[b]) ++b;
  while (b < n) {
    // get right stack
    c = b + 1;
    while (v[b] === v[c]) ++c;

    // are stacks adjacent?
    // if so, compare sizes and swap as needed
    if (v[b] - v[b - 1] < thresh) {
      d = b + (a + c - b - b >> 1);
      while (d < b) v[d++] = v[b];
      while (d > b) v[d--] = v[a];
    }

    // update left stack indices
    a = b;
    b = c;
  }
  return v;
}

function lcg (seed) {
  // Random numbers using a Linear Congruential Generator with seed value
  // Uses glibc values from https://en.wikipedia.org/wiki/Linear_congruential_generator
  return function () {
    seed = (1103515245 * seed + 12345) % 2147483647;
    return seed / 2147483647;
  };
}

function integer (min, max) {
  if (max == null) {
    max = min;
    min = 0;
  }
  let a, b, d;
  const dist = {
    min(_) {
      if (arguments.length) {
        a = _ || 0;
        d = b - a;
        return dist;
      } else {
        return a;
      }
    },
    max(_) {
      if (arguments.length) {
        b = _ || 0;
        d = b - a;
        return dist;
      } else {
        return b;
      }
    },
    sample() {
      return a + Math.floor(d * random());
    },
    pdf(x) {
      return x === Math.floor(x) && x >= a && x < b ? 1 / d : 0;
    },
    cdf(x) {
      const v = Math.floor(x);
      return v < a ? 0 : v >= b ? 1 : (v - a + 1) / d;
    },
    icdf(p) {
      return p >= 0 && p <= 1 ? a - 1 + Math.floor(p * d) : NaN;
    }
  };
  return dist.min(min).max(max);
}

const SQRT2PI = Math.sqrt(2 * Math.PI);
const SQRT2 = Math.SQRT2;

let nextSample = NaN;
function sampleNormal(mean, stdev) {
  mean = mean || 0;
  stdev = stdev == null ? 1 : stdev;
  let x = 0,
    y = 0,
    rds,
    c;
  if (nextSample === nextSample) {
    x = nextSample;
    nextSample = NaN;
  } else {
    do {
      x = random() * 2 - 1;
      y = random() * 2 - 1;
      rds = x * x + y * y;
    } while (rds === 0 || rds > 1);
    c = Math.sqrt(-2 * Math.log(rds) / rds); // Box-Muller transform
    x *= c;
    nextSample = y * c;
  }
  return mean + x * stdev;
}
function densityNormal(value, mean, stdev) {
  stdev = stdev == null ? 1 : stdev;
  const z = (value - (mean || 0)) / stdev;
  return Math.exp(-0.5 * z * z) / (stdev * SQRT2PI);
}

// Approximation from West (2009)
// Better Approximations to Cumulative Normal Functions
function cumulativeNormal(value, mean, stdev) {
  mean = mean || 0;
  stdev = stdev == null ? 1 : stdev;
  const z = (value - mean) / stdev,
    Z = Math.abs(z);
  let cd;
  if (Z > 37) {
    cd = 0;
  } else {
    const exp = Math.exp(-Z * Z / 2);
    let sum;
    if (Z < 7.07106781186547) {
      sum = 3.52624965998911e-02 * Z + 0.700383064443688;
      sum = sum * Z + 6.37396220353165;
      sum = sum * Z + 33.912866078383;
      sum = sum * Z + 112.079291497871;
      sum = sum * Z + 221.213596169931;
      sum = sum * Z + 220.206867912376;
      cd = exp * sum;
      sum = 8.83883476483184e-02 * Z + 1.75566716318264;
      sum = sum * Z + 16.064177579207;
      sum = sum * Z + 86.7807322029461;
      sum = sum * Z + 296.564248779674;
      sum = sum * Z + 637.333633378831;
      sum = sum * Z + 793.826512519948;
      sum = sum * Z + 440.413735824752;
      cd = cd / sum;
    } else {
      sum = Z + 0.65;
      sum = Z + 4 / sum;
      sum = Z + 3 / sum;
      sum = Z + 2 / sum;
      sum = Z + 1 / sum;
      cd = exp / sum / 2.506628274631;
    }
  }
  return z > 0 ? 1 - cd : cd;
}

// Approximation of Probit function using inverse error function.
function quantileNormal(p, mean, stdev) {
  if (p < 0 || p > 1) return NaN;
  return (mean || 0) + (stdev == null ? 1 : stdev) * SQRT2 * erfinv(2 * p - 1);
}

// Approximate inverse error function. Implementation from "Approximating
// the erfinv function" by Mike Giles, GPU Computing Gems, volume 2, 2010.
// Ported from Apache Commons Math, http://www.apache.org/licenses/LICENSE-2.0
function erfinv(x) {
  // beware that the logarithm argument must be
  // commputed as (1.0 - x) * (1.0 + x),
  // it must NOT be simplified as 1.0 - x * x as this
  // would induce rounding errors near the boundaries +/-1
  let w = -Math.log((1 - x) * (1 + x)),
    p;
  if (w < 6.25) {
    w -= 3.125;
    p = -364441206401782e-35;
    p = -16850591381820166e-35 + p * w;
    p = 1.2858480715256400167e-18 + p * w;
    p = 1.115787767802518096e-17 + p * w;
    p = -1333171662854621e-31 + p * w;
    p = 2.0972767875968561637e-17 + p * w;
    p = 6.6376381343583238325e-15 + p * w;
    p = -4054566272975207e-29 + p * w;
    p = -8151934197605472e-29 + p * w;
    p = 2.6335093153082322977e-12 + p * w;
    p = -12975133253453532e-27 + p * w;
    p = -5415412054294628e-26 + p * w;
    p = 1.051212273321532285e-09 + p * w;
    p = -4.112633980346984e-9 + p * w;
    p = -2.9070369957882005e-8 + p * w;
    p = 4.2347877827932403518e-07 + p * w;
    p = -13654692000834679e-22 + p * w;
    p = -13882523362786469e-21 + p * w;
    p = 0.0001867342080340571352 + p * w;
    p = -740702534166267e-18 + p * w;
    p = -0.006033670871430149 + p * w;
    p = 0.24015818242558961693 + p * w;
    p = 1.6536545626831027356 + p * w;
  } else if (w < 16.0) {
    w = Math.sqrt(w) - 3.25;
    p = 2.2137376921775787049e-09;
    p = 9.0756561938885390979e-08 + p * w;
    p = -2.7517406297064545e-7 + p * w;
    p = 1.8239629214389227755e-08 + p * w;
    p = 1.5027403968909827627e-06 + p * w;
    p = -4013867526981546e-21 + p * w;
    p = 2.9234449089955446044e-06 + p * w;
    p = 1.2475304481671778723e-05 + p * w;
    p = -47318229009055734e-21 + p * w;
    p = 6.8284851459573175448e-05 + p * w;
    p = 2.4031110387097893999e-05 + p * w;
    p = -3550375203628475e-19 + p * w;
    p = 0.00095328937973738049703 + p * w;
    p = -0.0016882755560235047 + p * w;
    p = 0.0024914420961078508066 + p * w;
    p = -0.003751208507569241 + p * w;
    p = 0.005370914553590063617 + p * w;
    p = 1.0052589676941592334 + p * w;
    p = 3.0838856104922207635 + p * w;
  } else if (Number.isFinite(w)) {
    w = Math.sqrt(w) - 5.0;
    p = -27109920616438573e-27;
    p = -2555641816996525e-25 + p * w;
    p = 1.5076572693500548083e-09 + p * w;
    p = -3.789465440126737e-9 + p * w;
    p = 7.6157012080783393804e-09 + p * w;
    p = -1.496002662714924e-8 + p * w;
    p = 2.9147953450901080826e-08 + p * w;
    p = -6.771199775845234e-8 + p * w;
    p = 2.2900482228026654717e-07 + p * w;
    p = -9.9298272942317e-7 + p * w;
    p = 4.5260625972231537039e-06 + p * w;
    p = -1968177810553167e-20 + p * w;
    p = 7.5995277030017761139e-05 + p * w;
    p = -21503011930044477e-20 + p * w;
    p = -13871931833623122e-20 + p * w;
    p = 1.0103004648645343977 + p * w;
    p = 4.8499064014085844221 + p * w;
  } else {
    p = Infinity;
  }
  return p * x;
}
function gaussian (mean, stdev) {
  let mu, sigma;
  const dist = {
    mean(_) {
      if (arguments.length) {
        mu = _ || 0;
        return dist;
      } else {
        return mu;
      }
    },
    stdev(_) {
      if (arguments.length) {
        sigma = _ == null ? 1 : _;
        return dist;
      } else {
        return sigma;
      }
    },
    sample: () => sampleNormal(mu, sigma),
    pdf: value => densityNormal(value, mu, sigma),
    cdf: value => cumulativeNormal(value, mu, sigma),
    icdf: p => quantileNormal(p, mu, sigma)
  };
  return dist.mean(mean).stdev(stdev);
}

function kde (support, bandwidth) {
  const kernel = gaussian();
  let n = 0;
  const dist = {
    data(_) {
      if (arguments.length) {
        support = _;
        n = _ ? _.length : 0;
        return dist.bandwidth(bandwidth);
      } else {
        return support;
      }
    },
    bandwidth(_) {
      if (!arguments.length) return bandwidth;
      bandwidth = _;
      if (!bandwidth && support) bandwidth = estimateBandwidth(support);
      return dist;
    },
    sample() {
      return support[~~(random() * n)] + bandwidth * kernel.sample();
    },
    pdf(x) {
      let y = 0,
        i = 0;
      for (; i < n; ++i) {
        y += kernel.pdf((x - support[i]) / bandwidth);
      }
      return y / bandwidth / n;
    },
    cdf(x) {
      let y = 0,
        i = 0;
      for (; i < n; ++i) {
        y += kernel.cdf((x - support[i]) / bandwidth);
      }
      return y / n;
    },
    icdf() {
      throw Error('KDE icdf not supported.');
    }
  };
  return dist.data(support);
}

function sampleLogNormal(mean, stdev) {
  mean = mean || 0;
  stdev = stdev == null ? 1 : stdev;
  return Math.exp(mean + sampleNormal() * stdev);
}
function densityLogNormal(value, mean, stdev) {
  if (value <= 0) return 0;
  mean = mean || 0;
  stdev = stdev == null ? 1 : stdev;
  const z = (Math.log(value) - mean) / stdev;
  return Math.exp(-0.5 * z * z) / (stdev * SQRT2PI * value);
}
function cumulativeLogNormal(value, mean, stdev) {
  return cumulativeNormal(Math.log(value), mean, stdev);
}
function quantileLogNormal(p, mean, stdev) {
  return Math.exp(quantileNormal(p, mean, stdev));
}
function lognormal (mean, stdev) {
  let mu, sigma;
  const dist = {
    mean(_) {
      if (arguments.length) {
        mu = _ || 0;
        return dist;
      } else {
        return mu;
      }
    },
    stdev(_) {
      if (arguments.length) {
        sigma = _ == null ? 1 : _;
        return dist;
      } else {
        return sigma;
      }
    },
    sample: () => sampleLogNormal(mu, sigma),
    pdf: value => densityLogNormal(value, mu, sigma),
    cdf: value => cumulativeLogNormal(value, mu, sigma),
    icdf: p => quantileLogNormal(p, mu, sigma)
  };
  return dist.mean(mean).stdev(stdev);
}

function mixture$1 (dists, weights) {
  let m = 0,
    w;
  function normalize(x) {
    const w = [];
    let sum = 0,
      i;
    for (i = 0; i < m; ++i) {
      sum += w[i] = x[i] == null ? 1 : +x[i];
    }
    for (i = 0; i < m; ++i) {
      w[i] /= sum;
    }
    return w;
  }
  const dist = {
    weights(_) {
      if (arguments.length) {
        w = normalize(weights = _ || []);
        return dist;
      }
      return weights;
    },
    distributions(_) {
      if (arguments.length) {
        if (_) {
          m = _.length;
          dists = _;
        } else {
          m = 0;
          dists = [];
        }
        return dist.weights(weights);
      }
      return dists;
    },
    sample() {
      const r = random();
      let d = dists[m - 1],
        v = w[0],
        i = 0;

      // first select distribution
      for (; i < m - 1; v += w[++i]) {
        if (r < v) {
          d = dists[i];
          break;
        }
      }
      // then sample from it
      return d.sample();
    },
    pdf(x) {
      let p = 0,
        i = 0;
      for (; i < m; ++i) {
        p += w[i] * dists[i].pdf(x);
      }
      return p;
    },
    cdf(x) {
      let p = 0,
        i = 0;
      for (; i < m; ++i) {
        p += w[i] * dists[i].cdf(x);
      }
      return p;
    },
    icdf() {
      throw Error('Mixture icdf not supported.');
    }
  };
  return dist.distributions(dists).weights(weights);
}

function sampleUniform(min, max) {
  if (max == null) {
    max = min == null ? 1 : min;
    min = 0;
  }
  return min + (max - min) * random();
}
function densityUniform(value, min, max) {
  if (max == null) {
    max = min == null ? 1 : min;
    min = 0;
  }
  return value >= min && value <= max ? 1 / (max - min) : 0;
}
function cumulativeUniform(value, min, max) {
  if (max == null) {
    max = min == null ? 1 : min;
    min = 0;
  }
  return value < min ? 0 : value > max ? 1 : (value - min) / (max - min);
}
function quantileUniform(p, min, max) {
  if (max == null) {
    max = min == null ? 1 : min;
    min = 0;
  }
  return p >= 0 && p <= 1 ? min + p * (max - min) : NaN;
}
function uniform (min, max) {
  let a, b;
  const dist = {
    min(_) {
      if (arguments.length) {
        a = _ || 0;
        return dist;
      } else {
        return a;
      }
    },
    max(_) {
      if (arguments.length) {
        b = _ == null ? 1 : _;
        return dist;
      } else {
        return b;
      }
    },
    sample: () => sampleUniform(a, b),
    pdf: value => densityUniform(value, a, b),
    cdf: value => cumulativeUniform(value, a, b),
    icdf: p => quantileUniform(p, a, b)
  };
  if (max == null) {
    max = min == null ? 1 : min;
    min = 0;
  }
  return dist.min(min).max(max);
}

function constant (data, x, y) {
  let mean = 0,
    n = 0;
  for (const d of data) {
    const val = y(d);
    if (x(d) == null || val == null || isNaN(val)) continue;
    mean += (val - mean) / ++n;
  }
  return {
    coef: [mean],
    predict: () => mean,
    rSquared: 0
  };
}

// Ordinary Least Squares
function ols (uX, uY, uXY, uX2) {
  const delta = uX2 - uX * uX,
    slope = Math.abs(delta) < 1e-24 ? 0 : (uXY - uX * uY) / delta,
    intercept = uY - slope * uX;
  return [intercept, slope];
}

function points(data, x, y, sort) {
  data = data.filter(d => {
    let u = x(d),
      v = y(d);
    return u != null && (u = +u) >= u && v != null && (v = +v) >= v;
  });
  if (sort) {
    data.sort((a, b) => x(a) - x(b));
  }
  const n = data.length,
    X = new Float64Array(n),
    Y = new Float64Array(n);

  // extract values, calculate means
  let i = 0,
    ux = 0,
    uy = 0,
    xv,
    yv,
    d;
  for (d of data) {
    X[i] = xv = +x(d);
    Y[i] = yv = +y(d);
    ++i;
    ux += (xv - ux) / i;
    uy += (yv - uy) / i;
  }

  // mean center the data
  for (i = 0; i < n; ++i) {
    X[i] -= ux;
    Y[i] -= uy;
  }
  return [X, Y, ux, uy];
}
function visitPoints(data, x, y, callback) {
  let i = -1,
    u,
    v;
  for (const d of data) {
    u = x(d);
    v = y(d);
    if (u != null && (u = +u) >= u && v != null && (v = +v) >= v) {
      callback(u, v, ++i);
    }
  }
}

// Adapted from d3-regression by Harry Stevens
// License: https://github.com/HarryStevens/d3-regression/blob/master/LICENSE
function rSquared (data, x, y, uY, predict) {
  let SSE = 0,
    SST = 0;
  visitPoints(data, x, y, (dx, dy) => {
    const sse = dy - predict(dx),
      sst = dy - uY;
    SSE += sse * sse;
    SST += sst * sst;
  });
  return 1 - SSE / SST;
}

// Adapted from d3-regression by Harry Stevens
// License: https://github.com/HarryStevens/d3-regression/blob/master/LICENSE
function linear (data, x, y) {
  let X = 0,
    Y = 0,
    XY = 0,
    X2 = 0,
    n = 0;
  visitPoints(data, x, y, (dx, dy) => {
    ++n;
    X += (dx - X) / n;
    Y += (dy - Y) / n;
    XY += (dx * dy - XY) / n;
    X2 += (dx * dx - X2) / n;
  });
  const coef = ols(X, Y, XY, X2),
    predict = x => coef[0] + coef[1] * x;
  return {
    coef: coef,
    predict: predict,
    rSquared: rSquared(data, x, y, Y, predict)
  };
}

// Adapted from d3-regression by Harry Stevens
// License: https://github.com/HarryStevens/d3-regression/blob/master/LICENSE
function log$1 (data, x, y) {
  let X = 0,
    Y = 0,
    XY = 0,
    X2 = 0,
    n = 0;
  visitPoints(data, x, y, (dx, dy) => {
    ++n;
    dx = Math.log(dx);
    X += (dx - X) / n;
    Y += (dy - Y) / n;
    XY += (dx * dy - XY) / n;
    X2 += (dx * dx - X2) / n;
  });
  const coef = ols(X, Y, XY, X2),
    predict = x => coef[0] + coef[1] * Math.log(x);
  return {
    coef: coef,
    predict: predict,
    rSquared: rSquared(data, x, y, Y, predict)
  };
}

function exp (data, x, y) {
  const [xv, yv, ux, uy] = points(data, x, y);
  let YL = 0,
    XY = 0,
    XYL = 0,
    X2Y = 0,
    n = 0,
    dx,
    ly,
    xy;
  visitPoints(data, x, y, (_, dy) => {
    dx = xv[n++];
    ly = Math.log(dy);
    xy = dx * dy;
    YL += (dy * ly - YL) / n;
    XY += (xy - XY) / n;
    XYL += (xy * ly - XYL) / n;
    X2Y += (dx * xy - X2Y) / n;
  });
  const [c0, c1] = ols(XY / uy, YL / uy, XYL / uy, X2Y / uy),
    predict = x => Math.exp(c0 + c1 * (x - ux));
  return {
    coef: [Math.exp(c0 - c1 * ux), c1],
    predict: predict,
    rSquared: rSquared(data, x, y, uy, predict)
  };
}

// Adapted from d3-regression by Harry Stevens
// License: https://github.com/HarryStevens/d3-regression/blob/master/LICENSE
function pow (data, x, y) {
  let X = 0,
    Y = 0,
    XY = 0,
    X2 = 0,
    YS = 0,
    n = 0;
  visitPoints(data, x, y, (dx, dy) => {
    const lx = Math.log(dx),
      ly = Math.log(dy);
    ++n;
    X += (lx - X) / n;
    Y += (ly - Y) / n;
    XY += (lx * ly - XY) / n;
    X2 += (lx * lx - X2) / n;
    YS += (dy - YS) / n;
  });
  const coef = ols(X, Y, XY, X2),
    predict = x => coef[0] * Math.pow(x, coef[1]);
  coef[0] = Math.exp(coef[0]);
  return {
    coef: coef,
    predict: predict,
    rSquared: rSquared(data, x, y, YS, predict)
  };
}

function quad (data, x, y) {
  const [xv, yv, ux, uy] = points(data, x, y),
    n = xv.length;
  let X2 = 0,
    X3 = 0,
    X4 = 0,
    XY = 0,
    X2Y = 0,
    i,
    dx,
    dy,
    x2;
  for (i = 0; i < n;) {
    dx = xv[i];
    dy = yv[i++];
    x2 = dx * dx;
    X2 += (x2 - X2) / i;
    X3 += (x2 * dx - X3) / i;
    X4 += (x2 * x2 - X4) / i;
    XY += (dx * dy - XY) / i;
    X2Y += (x2 * dy - X2Y) / i;
  }
  const X2X2 = X4 - X2 * X2,
    d = X2 * X2X2 - X3 * X3,
    a = (X2Y * X2 - XY * X3) / d,
    b = (XY * X2X2 - X2Y * X3) / d,
    c = -a * X2,
    predict = x => {
      x = x - ux;
      return a * x * x + b * x + c + uy;
    };

  // transform coefficients back from mean-centered space
  return {
    coef: [c - b * ux + a * ux * ux + uy, b - 2 * a * ux, a],
    predict: predict,
    rSquared: rSquared(data, x, y, uy, predict)
  };
}

// Adapted from d3-regression by Harry Stevens
// License: https://github.com/HarryStevens/d3-regression/blob/master/LICENSE
// ... which was adapted from regression-js by Tom Alexander
// Source: https://github.com/Tom-Alexander/regression-js/blob/master/src/regression.js#L246
// License: https://github.com/Tom-Alexander/regression-js/blob/master/LICENSE
function poly (data, x, y, order) {
  // use more efficient methods for lower orders
  if (order === 0) return constant(data, x, y);
  if (order === 1) return linear(data, x, y);
  if (order === 2) return quad(data, x, y);
  const [xv, yv, ux, uy] = points(data, x, y),
    n = xv.length,
    lhs = [],
    rhs = [],
    k = order + 1;
  let i, j, l, v, c;
  for (i = 0; i < k; ++i) {
    for (l = 0, v = 0; l < n; ++l) {
      v += Math.pow(xv[l], i) * yv[l];
    }
    lhs.push(v);
    c = new Float64Array(k);
    for (j = 0; j < k; ++j) {
      for (l = 0, v = 0; l < n; ++l) {
        v += Math.pow(xv[l], i + j);
      }
      c[j] = v;
    }
    rhs.push(c);
  }
  rhs.push(lhs);
  const coef = gaussianElimination(rhs),
    predict = x => {
      x -= ux;
      let y = uy + coef[0] + coef[1] * x + coef[2] * x * x;
      for (i = 3; i < k; ++i) y += coef[i] * Math.pow(x, i);
      return y;
    };
  return {
    coef: uncenter(k, coef, -ux, uy),
    predict: predict,
    rSquared: rSquared(data, x, y, uy, predict)
  };
}
function uncenter(k, a, x, y) {
  const z = Array(k);
  let i, j, v, c;

  // initialize to zero
  for (i = 0; i < k; ++i) z[i] = 0;

  // polynomial expansion
  for (i = k - 1; i >= 0; --i) {
    v = a[i];
    c = 1;
    z[i] += v;
    for (j = 1; j <= i; ++j) {
      c *= (i + 1 - j) / j; // binomial coefficent
      z[i - j] += v * Math.pow(x, j) * c;
    }
  }

  // bias term
  z[0] += y;
  return z;
}

// Given an array for a two-dimensional matrix and the polynomial order,
// solve A * x = b using Gaussian elimination.
function gaussianElimination(matrix) {
  const n = matrix.length - 1,
    coef = [];
  let i, j, k, r, t;
  for (i = 0; i < n; ++i) {
    r = i; // max row
    for (j = i + 1; j < n; ++j) {
      if (Math.abs(matrix[i][j]) > Math.abs(matrix[i][r])) {
        r = j;
      }
    }
    for (k = i; k < n + 1; ++k) {
      t = matrix[k][i];
      matrix[k][i] = matrix[k][r];
      matrix[k][r] = t;
    }
    for (j = i + 1; j < n; ++j) {
      for (k = n; k >= i; k--) {
        matrix[k][j] -= matrix[k][i] * matrix[i][j] / matrix[i][i];
      }
    }
  }
  for (j = n - 1; j >= 0; --j) {
    t = 0;
    for (k = j + 1; k < n; ++k) {
      t += matrix[k][j] * coef[k];
    }
    coef[j] = (matrix[n][j] - t) / matrix[j][j];
  }
  return coef;
}

const maxiters = 2,
  epsilon = 1e-12;

// Adapted from science.js by Jason Davies
// Source: https://github.com/jasondavies/science.js/blob/master/src/stats/loess.js
// License: https://github.com/jasondavies/science.js/blob/master/LICENSE
function loess (data, x, y, bandwidth) {
  const [xv, yv, ux, uy] = points(data, x, y, true),
    n = xv.length,
    bw = Math.max(2, ~~(bandwidth * n)),
    // # nearest neighbors
    yhat = new Float64Array(n),
    residuals = new Float64Array(n),
    robustWeights = new Float64Array(n).fill(1);
  for (let iter = -1; ++iter <= maxiters;) {
    const interval = [0, bw - 1];
    for (let i = 0; i < n; ++i) {
      const dx = xv[i],
        i0 = interval[0],
        i1 = interval[1],
        edge = dx - xv[i0] > xv[i1] - dx ? i0 : i1;
      let W = 0,
        X = 0,
        Y = 0,
        XY = 0,
        X2 = 0;
      const denom = 1 / Math.abs(xv[edge] - dx || 1); // avoid singularity!

      for (let k = i0; k <= i1; ++k) {
        const xk = xv[k],
          yk = yv[k],
          w = tricube(Math.abs(dx - xk) * denom) * robustWeights[k],
          xkw = xk * w;
        W += w;
        X += xkw;
        Y += yk * w;
        XY += yk * xkw;
        X2 += xk * xkw;
      }

      // linear regression fit
      const [a, b] = ols(X / W, Y / W, XY / W, X2 / W);
      yhat[i] = a + b * dx;
      residuals[i] = Math.abs(yv[i] - yhat[i]);
      updateInterval(xv, i + 1, interval);
    }
    if (iter === maxiters) {
      break;
    }
    const medianResidual = median(residuals);
    if (Math.abs(medianResidual) < epsilon) break;
    for (let i = 0, arg, w; i < n; ++i) {
      arg = residuals[i] / (6 * medianResidual);
      // default to epsilon (rather than zero) for large deviations
      // keeping weights tiny but non-zero prevents singularites
      robustWeights[i] = arg >= 1 ? epsilon : (w = 1 - arg * arg) * w;
    }
  }
  return output$1(xv, yhat, ux, uy);
}

// weighting kernel for local regression
function tricube(x) {
  return (x = 1 - x * x * x) * x * x;
}

// advance sliding window interval of nearest neighbors
function updateInterval(xv, i, interval) {
  const val = xv[i];
  let left = interval[0],
    right = interval[1] + 1;
  if (right >= xv.length) return;

  // step right if distance to new right edge is <= distance to old left edge
  // step when distance is equal to ensure movement over duplicate x values
  while (i > left && xv[right] - val <= val - xv[left]) {
    interval[0] = ++left;
    interval[1] = right;
    ++right;
  }
}

// generate smoothed output points
// average points with repeated x values
function output$1(xv, yhat, ux, uy) {
  const n = xv.length,
    out = [];
  let i = 0,
    cnt = 0,
    prev = [],
    v;
  for (; i < n; ++i) {
    v = xv[i] + ux;
    if (prev[0] === v) {
      // average output values via online update
      prev[1] += (yhat[i] - prev[1]) / ++cnt;
    } else {
      // add new output point
      cnt = 0;
      prev[1] += uy;
      prev = [v, yhat[i]];
      out.push(prev);
    }
  }
  prev[1] += uy;
  return out;
}

// subdivide up to accuracy of 0.5 degrees
const MIN_RADIANS = 0.5 * Math.PI / 180;

// Adaptively sample an interpolated function over a domain extent
function sampleCurve (f, extent, minSteps, maxSteps) {
  minSteps = minSteps || 25;
  maxSteps = Math.max(minSteps, maxSteps || 200);
  const point = x => [x, f(x)],
    minX = extent[0],
    maxX = extent[1],
    span = maxX - minX,
    stop = span / maxSteps,
    prev = [point(minX)],
    next = [];
  if (minSteps === maxSteps) {
    // no adaptation, sample uniform grid directly and return
    for (let i = 1; i < maxSteps; ++i) {
      prev.push(point(minX + i / minSteps * span));
    }
    prev.push(point(maxX));
    return prev;
  } else {
    // sample minimum points on uniform grid
    // then move on to perform adaptive refinement
    next.push(point(maxX));
    for (let i = minSteps; --i > 0;) {
      next.push(point(minX + i / minSteps * span));
    }
  }
  let p0 = prev[0];
  let p1 = next[next.length - 1];
  const sx = 1 / span;
  const sy = scaleY(p0[1], next);
  while (p1) {
    // midpoint for potential curve subdivision
    const pm = point((p0[0] + p1[0]) / 2);
    const dx = pm[0] - p0[0] >= stop;
    if (dx && angleDelta(p0, pm, p1, sx, sy) > MIN_RADIANS) {
      // maximum resolution has not yet been met, and
      // subdivision midpoint is sufficiently different from endpoint
      // save subdivision, push midpoint onto the visitation stack
      next.push(pm);
    } else {
      // subdivision midpoint sufficiently similar to endpoint
      // skip subdivision, store endpoint, move to next point on the stack
      p0 = p1;
      prev.push(p1);
      next.pop();
    }
    p1 = next[next.length - 1];
  }
  return prev;
}
function scaleY(init, points) {
  let ymin = init;
  let ymax = init;
  const n = points.length;
  for (let i = 0; i < n; ++i) {
    const y = points[i][1];
    if (y < ymin) ymin = y;
    if (y > ymax) ymax = y;
  }
  return 1 / (ymax - ymin);
}
function angleDelta(p, q, r, sx, sy) {
  const a0 = Math.atan2(sy * (r[1] - p[1]), sx * (r[0] - p[0])),
    a1 = Math.atan2(sy * (q[1] - p[1]), sx * (q[0] - p[0]));
  return Math.abs(a0 - a1);
}

function multikey(f) {
  return x => {
    const n = f.length;
    let i = 1,
      k = String(f[0](x));
    for (; i < n; ++i) {
      k += '|' + f[i](x);
    }
    return k;
  };
}
function groupkey(fields) {
  return !fields || !fields.length ? function () {
    return '';
  } : fields.length === 1 ? fields[0] : multikey(fields);
}

function measureName(op, field, as) {
  return as || op + (!field ? '' : '_' + field);
}
const noop$1 = () => {};
const base_op = {
  init: noop$1,
  add: noop$1,
  rem: noop$1,
  idx: 0
};
const AggregateOps = {
  values: {
    init: m => m.cell.store = true,
    value: m => m.cell.data.values(),
    idx: -1
  },
  count: {
    value: m => m.cell.num
  },
  __count__: {
    value: m => m.missing + m.valid
  },
  missing: {
    value: m => m.missing
  },
  valid: {
    value: m => m.valid
  },
  sum: {
    init: m => m.sum = 0,
    value: m => m.valid ? m.sum : undefined,
    add: (m, v) => m.sum += +v,
    rem: (m, v) => m.sum -= v
  },
  product: {
    init: m => m.product = 1,
    value: m => m.valid ? m.product : undefined,
    add: (m, v) => m.product *= v,
    rem: (m, v) => m.product /= v
  },
  mean: {
    init: m => m.mean = 0,
    value: m => m.valid ? m.mean : undefined,
    add: (m, v) => (m.mean_d = v - m.mean, m.mean += m.mean_d / m.valid),
    rem: (m, v) => (m.mean_d = v - m.mean, m.mean -= m.valid ? m.mean_d / m.valid : m.mean)
  },
  average: {
    value: m => m.valid ? m.mean : undefined,
    req: ['mean'],
    idx: 1
  },
  variance: {
    init: m => m.dev = 0,
    value: m => m.valid > 1 ? m.dev / (m.valid - 1) : undefined,
    add: (m, v) => m.dev += m.mean_d * (v - m.mean),
    rem: (m, v) => m.dev -= m.mean_d * (v - m.mean),
    req: ['mean'],
    idx: 1
  },
  variancep: {
    value: m => m.valid > 1 ? m.dev / m.valid : undefined,
    req: ['variance'],
    idx: 2
  },
  stdev: {
    value: m => m.valid > 1 ? Math.sqrt(m.dev / (m.valid - 1)) : undefined,
    req: ['variance'],
    idx: 2
  },
  stdevp: {
    value: m => m.valid > 1 ? Math.sqrt(m.dev / m.valid) : undefined,
    req: ['variance'],
    idx: 2
  },
  stderr: {
    value: m => m.valid > 1 ? Math.sqrt(m.dev / (m.valid * (m.valid - 1))) : undefined,
    req: ['variance'],
    idx: 2
  },
  distinct: {
    value: m => m.cell.data.distinct(m.get),
    req: ['values'],
    idx: 3
  },
  ci0: {
    value: m => m.cell.data.ci0(m.get),
    req: ['values'],
    idx: 3
  },
  ci1: {
    value: m => m.cell.data.ci1(m.get),
    req: ['values'],
    idx: 3
  },
  median: {
    value: m => m.cell.data.q2(m.get),
    req: ['values'],
    idx: 3
  },
  q1: {
    value: m => m.cell.data.q1(m.get),
    req: ['values'],
    idx: 3
  },
  q3: {
    value: m => m.cell.data.q3(m.get),
    req: ['values'],
    idx: 3
  },
  min: {
    init: m => m.min = undefined,
    value: m => m.min = Number.isNaN(m.min) ? m.cell.data.min(m.get) : m.min,
    add: (m, v) => {
      if (v < m.min || m.min === undefined) m.min = v;
    },
    rem: (m, v) => {
      if (v <= m.min) m.min = NaN;
    },
    req: ['values'],
    idx: 4
  },
  max: {
    init: m => m.max = undefined,
    value: m => m.max = Number.isNaN(m.max) ? m.cell.data.max(m.get) : m.max,
    add: (m, v) => {
      if (v > m.max || m.max === undefined) m.max = v;
    },
    rem: (m, v) => {
      if (v >= m.max) m.max = NaN;
    },
    req: ['values'],
    idx: 4
  },
  argmin: {
    init: m => m.argmin = undefined,
    value: m => m.argmin || m.cell.data.argmin(m.get),
    add: (m, v, t) => {
      if (v < m.min) m.argmin = t;
    },
    rem: (m, v) => {
      if (v <= m.min) m.argmin = undefined;
    },
    req: ['min', 'values'],
    idx: 3
  },
  argmax: {
    init: m => m.argmax = undefined,
    value: m => m.argmax || m.cell.data.argmax(m.get),
    add: (m, v, t) => {
      if (v > m.max) m.argmax = t;
    },
    rem: (m, v) => {
      if (v >= m.max) m.argmax = undefined;
    },
    req: ['max', 'values'],
    idx: 3
  },
  exponential: {
    init: (m, r) => {
      m.exp = 0;
      m.exp_r = r;
    },
    value: m => m.valid ? m.exp * (1 - m.exp_r) / (1 - m.exp_r ** m.valid) : undefined,
    add: (m, v) => m.exp = m.exp_r * m.exp + v,
    rem: (m, v) => m.exp = (m.exp - v / m.exp_r ** (m.valid - 1)) / m.exp_r
  },
  exponentialb: {
    value: m => m.valid ? m.exp * (1 - m.exp_r) : undefined,
    req: ['exponential'],
    idx: 1
  }
};
const ValidAggregateOps = Object.keys(AggregateOps).filter(d => d !== '__count__');
function measure(key, value) {
  return (out, aggregate_param) => extend({
    name: key,
    aggregate_param: aggregate_param,
    out: out || key
  }, base_op, value);
}
[...ValidAggregateOps, '__count__'].forEach(key => {
  AggregateOps[key] = measure(key, AggregateOps[key]);
});
function createMeasure(op, param, name) {
  return AggregateOps[op](name, param);
}
function compareIndex(a, b) {
  return a.idx - b.idx;
}
function resolve(agg) {
  const map = {};
  agg.forEach(a => map[a.name] = a);
  const getreqs = a => {
    if (!a.req) return;
    a.req.forEach(key => {
      if (!map[key]) getreqs(map[key] = AggregateOps[key]());
    });
  };
  agg.forEach(getreqs);
  return Object.values(map).sort(compareIndex);
}
function init() {
  this.valid = 0;
  this.missing = 0;
  this._ops.forEach(op => op.aggregate_param == null ? op.init(this) : op.init(this, op.aggregate_param));
}
function add$2(v, t) {
  if (v == null || v === '') {
    ++this.missing;
    return;
  }
  if (v !== v) return;
  ++this.valid;
  this._ops.forEach(op => op.add(this, v, t));
}
function rem(v, t) {
  if (v == null || v === '') {
    --this.missing;
    return;
  }
  if (v !== v) return;
  --this.valid;
  this._ops.forEach(op => op.rem(this, v, t));
}
function set$3(t) {
  this._out.forEach(op => t[op.out] = op.value(this));
  return t;
}
function compileMeasures(agg, field) {
  const get = field || identity,
    ops = resolve(agg),
    out = agg.slice().sort(compareIndex);
  function ctr(cell) {
    this._ops = ops;
    this._out = out;
    this.cell = cell;
    this.init();
  }
  ctr.prototype.init = init;
  ctr.prototype.add = add$2;
  ctr.prototype.rem = rem;
  ctr.prototype.set = set$3;
  ctr.prototype.get = get;
  ctr.fields = agg.map(op => op.out);
  return ctr;
}

function TupleStore(key) {
  this._key = key ? field$1(key) : tupleid;
  this.reset();
}
const prototype$1 = TupleStore.prototype;
prototype$1.reset = function () {
  this._add = [];
  this._rem = [];
  this._ext = null;
  this._get = null;
  this._q = null;
};
prototype$1.add = function (v) {
  this._add.push(v);
};
prototype$1.rem = function (v) {
  this._rem.push(v);
};
prototype$1.values = function () {
  this._get = null;
  if (this._rem.length === 0) return this._add;
  const a = this._add,
    r = this._rem,
    k = this._key,
    n = a.length,
    m = r.length,
    x = Array(n - m),
    map = {};
  let i, j, v;

  // use unique key field to clear removed values
  for (i = 0; i < m; ++i) {
    map[k(r[i])] = 1;
  }
  for (i = 0, j = 0; i < n; ++i) {
    if (map[k(v = a[i])]) {
      map[k(v)] = 0;
    } else {
      x[j++] = v;
    }
  }
  this._rem = [];
  return this._add = x;
};

// memoizing statistics methods

prototype$1.distinct = function (get) {
  const v = this.values(),
    map = {};
  let n = v.length,
    count = 0,
    s;
  while (--n >= 0) {
    s = get(v[n]) + '';
    if (!has$1(map, s)) {
      map[s] = 1;
      ++count;
    }
  }
  return count;
};
prototype$1.extent = function (get) {
  if (this._get !== get || !this._ext) {
    const v = this.values(),
      i = extentIndex(v, get);
    this._ext = [v[i[0]], v[i[1]]];
    this._get = get;
  }
  return this._ext;
};
prototype$1.argmin = function (get) {
  return this.extent(get)[0] || {};
};
prototype$1.argmax = function (get) {
  return this.extent(get)[1] || {};
};
prototype$1.min = function (get) {
  const m = this.extent(get)[0];
  return m != null ? get(m) : undefined;
};
prototype$1.max = function (get) {
  const m = this.extent(get)[1];
  return m != null ? get(m) : undefined;
};
prototype$1.quartile = function (get) {
  if (this._get !== get || !this._q) {
    this._q = quartiles(this.values(), get);
    this._get = get;
  }
  return this._q;
};
prototype$1.q1 = function (get) {
  return this.quartile(get)[0];
};
prototype$1.q2 = function (get) {
  return this.quartile(get)[1];
};
prototype$1.q3 = function (get) {
  return this.quartile(get)[2];
};
prototype$1.ci = function (get) {
  if (this._get !== get || !this._ci) {
    this._ci = bootstrapCI(this.values(), 1000, 0.05, get);
    this._get = get;
  }
  return this._ci;
};
prototype$1.ci0 = function (get) {
  return this.ci(get)[0];
};
prototype$1.ci1 = function (get) {
  return this.ci(get)[1];
};

/**
 * Group-by aggregation operator.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors to groupby.
 * @param {Array<function(object): *>} [params.fields] - An array of accessors to aggregate.
 * @param {Array<string>} [params.ops] - An array of strings indicating aggregation operations.
 * @param {Array<number>} [params.aggregate_params] - An optional array of parameters for aggregation operations.
 * @param {Array<string>} [params.as] - An array of output field names for aggregated values.
 * @param {boolean} [params.cross=false] - A flag indicating that the full
 *   cross-product of groupby values should be generated, including empty cells.
 *   If true, the drop parameter is ignored and empty cells are retained.
 * @param {boolean} [params.drop=true] - A flag indicating if empty cells should be removed.
 */
function Aggregate$1(params) {
  Transform.call(this, null, params);
  this._adds = []; // array of added output tuples
  this._mods = []; // array of modified output tuples
  this._alen = 0; // number of active added tuples
  this._mlen = 0; // number of active modified tuples
  this._drop = true; // should empty aggregation cells be removed
  this._cross = false; // produce full cross-product of group-by values

  this._dims = []; // group-by dimension accessors
  this._dnames = []; // group-by dimension names

  this._measures = []; // collection of aggregation monoids
  this._countOnly = false; // flag indicating only count aggregation
  this._counts = null; // collection of count fields
  this._prev = null; // previous aggregation cells

  this._inputs = null; // array of dependent input tuple field names
  this._outputs = null; // array of output tuple field names
}
Aggregate$1.Definition = {
  'type': 'Aggregate',
  'metadata': {
    'generates': true,
    'changes': true
  },
  'params': [{
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'ops',
    'type': 'enum',
    'array': true,
    'values': ValidAggregateOps
  }, {
    'name': 'aggregate_params',
    'type': 'number',
    'null': true,
    'array': true
  }, {
    'name': 'fields',
    'type': 'field',
    'null': true,
    'array': true
  }, {
    'name': 'as',
    'type': 'string',
    'null': true,
    'array': true
  }, {
    'name': 'drop',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'cross',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'key',
    'type': 'field'
  }]
};
inherits(Aggregate$1, Transform, {
  transform(_, pulse) {
    const aggr = this,
      out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      mod = _.modified();
    aggr.stamp = out.stamp;
    if (aggr.value && (mod || pulse.modified(aggr._inputs, true))) {
      aggr._prev = aggr.value;
      aggr.value = mod ? aggr.init(_) : Object.create(null);
      pulse.visit(pulse.SOURCE, t => aggr.add(t));
    } else {
      aggr.value = aggr.value || aggr.init(_);
      pulse.visit(pulse.REM, t => aggr.rem(t));
      pulse.visit(pulse.ADD, t => aggr.add(t));
    }

    // Indicate output fields and return aggregate tuples.
    out.modifies(aggr._outputs);

    // Should empty cells be dropped?
    aggr._drop = _.drop !== false;

    // If domain cross-product requested, generate empty cells as needed
    // and ensure that empty cells are not dropped
    if (_.cross && aggr._dims.length > 1) {
      aggr._drop = false;
      aggr.cross();
    }
    if (pulse.clean() && aggr._drop) {
      out.clean(true).runAfter(() => this.clean());
    }
    return aggr.changes(out);
  },
  cross() {
    const aggr = this,
      curr = aggr.value,
      dims = aggr._dnames,
      vals = dims.map(() => ({})),
      n = dims.length;

    // collect all group-by domain values
    function collect(cells) {
      let key, i, t, v;
      for (key in cells) {
        t = cells[key].tuple;
        for (i = 0; i < n; ++i) {
          vals[i][v = t[dims[i]]] = v;
        }
      }
    }
    collect(aggr._prev);
    collect(curr);

    // iterate over key cross-product, create cells as needed
    function generate(base, tuple, index) {
      const name = dims[index],
        v = vals[index++];
      for (const k in v) {
        const key = base ? base + '|' + k : k;
        tuple[name] = v[k];
        if (index < n) generate(key, tuple, index);else if (!curr[key]) aggr.cell(key, tuple);
      }
    }
    generate('', {}, 0);
  },
  init(_) {
    // initialize input and output fields
    const inputs = this._inputs = [],
      outputs = this._outputs = [],
      inputMap = {};
    function inputVisit(get) {
      const fields = array$2(accessorFields(get)),
        n = fields.length;
      let i = 0,
        f;
      for (; i < n; ++i) {
        if (!inputMap[f = fields[i]]) {
          inputMap[f] = 1;
          inputs.push(f);
        }
      }
    }

    // initialize group-by dimensions
    this._dims = array$2(_.groupby);
    this._dnames = this._dims.map(d => {
      const dname = accessorName(d);
      inputVisit(d);
      outputs.push(dname);
      return dname;
    });
    this.cellkey = _.key ? _.key : groupkey(this._dims);

    // initialize aggregate measures
    this._countOnly = true;
    this._counts = [];
    this._measures = [];
    const fields = _.fields || [null],
      ops = _.ops || ['count'],
      aggregate_params = _.aggregate_params || [null],
      as = _.as || [],
      n = fields.length,
      map = {};
    let field, op, aggregate_param, m, mname, outname, i;
    if (n !== ops.length) {
      error$1('Unmatched number of fields and aggregate ops.');
    }
    for (i = 0; i < n; ++i) {
      field = fields[i];
      op = ops[i];
      aggregate_param = aggregate_params[i] || null;
      if (field == null && op !== 'count') {
        error$1('Null aggregate field specified.');
      }
      mname = accessorName(field);
      outname = measureName(op, mname, as[i]);
      outputs.push(outname);
      if (op === 'count') {
        this._counts.push(outname);
        continue;
      }
      m = map[mname];
      if (!m) {
        inputVisit(field);
        m = map[mname] = [];
        m.field = field;
        this._measures.push(m);
      }
      if (op !== 'count') this._countOnly = false;
      m.push(createMeasure(op, aggregate_param, outname));
    }
    this._measures = this._measures.map(m => compileMeasures(m, m.field));
    return Object.create(null); // aggregation cells (this.value)
  },
  // -- Cell Management -----

  cellkey: groupkey(),
  cell(key, t) {
    let cell = this.value[key];
    if (!cell) {
      cell = this.value[key] = this.newcell(key, t);
      this._adds[this._alen++] = cell;
    } else if (cell.num === 0 && this._drop && cell.stamp < this.stamp) {
      cell.stamp = this.stamp;
      this._adds[this._alen++] = cell;
    } else if (cell.stamp < this.stamp) {
      cell.stamp = this.stamp;
      this._mods[this._mlen++] = cell;
    }
    return cell;
  },
  newcell(key, t) {
    const cell = {
      key: key,
      num: 0,
      agg: null,
      tuple: this.newtuple(t, this._prev && this._prev[key]),
      stamp: this.stamp,
      store: false
    };
    if (!this._countOnly) {
      const measures = this._measures,
        n = measures.length;
      cell.agg = Array(n);
      for (let i = 0; i < n; ++i) {
        cell.agg[i] = new measures[i](cell);
      }
    }
    if (cell.store) {
      cell.data = new TupleStore();
    }
    return cell;
  },
  newtuple(t, p) {
    const names = this._dnames,
      dims = this._dims,
      n = dims.length,
      x = {};
    for (let i = 0; i < n; ++i) {
      x[names[i]] = dims[i](t);
    }
    return p ? replace$1(p.tuple, x) : ingest$1(x);
  },
  clean() {
    const cells = this.value;
    for (const key in cells) {
      if (cells[key].num === 0) {
        delete cells[key];
      }
    }
  },
  // -- Process Tuples -----

  add(t) {
    const key = this.cellkey(t),
      cell = this.cell(key, t);
    cell.num += 1;
    if (this._countOnly) return;
    if (cell.store) cell.data.add(t);
    const agg = cell.agg;
    for (let i = 0, n = agg.length; i < n; ++i) {
      agg[i].add(agg[i].get(t), t);
    }
  },
  rem(t) {
    const key = this.cellkey(t),
      cell = this.cell(key, t);
    cell.num -= 1;
    if (this._countOnly) return;
    if (cell.store) cell.data.rem(t);
    const agg = cell.agg;
    for (let i = 0, n = agg.length; i < n; ++i) {
      agg[i].rem(agg[i].get(t), t);
    }
  },
  celltuple(cell) {
    const tuple = cell.tuple,
      counts = this._counts;

    // consolidate stored values
    if (cell.store) {
      cell.data.values();
    }

    // update tuple properties
    for (let i = 0, n = counts.length; i < n; ++i) {
      tuple[counts[i]] = cell.num;
    }
    if (!this._countOnly) {
      const agg = cell.agg;
      for (let i = 0, n = agg.length; i < n; ++i) {
        agg[i].set(tuple);
      }
    }
    return tuple;
  },
  changes(out) {
    const adds = this._adds,
      mods = this._mods,
      prev = this._prev,
      drop = this._drop,
      add = out.add,
      rem = out.rem,
      mod = out.mod;
    let cell, key, i, n;
    if (prev) for (key in prev) {
      cell = prev[key];
      if (!drop || cell.num) rem.push(cell.tuple);
    }
    for (i = 0, n = this._alen; i < n; ++i) {
      add.push(this.celltuple(adds[i]));
      adds[i] = null; // for garbage collection
    }
    for (i = 0, n = this._mlen; i < n; ++i) {
      cell = mods[i];
      (cell.num === 0 && drop ? rem : mod).push(this.celltuple(cell));
      mods[i] = null; // for garbage collection
    }
    this._alen = this._mlen = 0; // reset list of active cells
    this._prev = null;
    return out;
  }
});

// epsilon bias to offset floating point error (#1737)
const EPSILON$1 = 1e-14;

/**
 * Generates a binning function for discretizing data.
 * @constructor
 * @param {object} params - The parameters for this operator. The
 *   provided values should be valid options for the {@link bin} function.
 * @param {function(object): *} params.field - The data field to bin.
 */
function Bin(params) {
  Transform.call(this, null, params);
}
Bin.Definition = {
  'type': 'Bin',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'interval',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'anchor',
    'type': 'number'
  }, {
    'name': 'maxbins',
    'type': 'number',
    'default': 20
  }, {
    'name': 'base',
    'type': 'number',
    'default': 10
  }, {
    'name': 'divide',
    'type': 'number',
    'array': true,
    'default': [5, 2]
  }, {
    'name': 'extent',
    'type': 'number',
    'array': true,
    'length': 2,
    'required': true
  }, {
    'name': 'span',
    'type': 'number'
  }, {
    'name': 'step',
    'type': 'number'
  }, {
    'name': 'steps',
    'type': 'number',
    'array': true
  }, {
    'name': 'minstep',
    'type': 'number',
    'default': 0
  }, {
    'name': 'nice',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'name',
    'type': 'string'
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': ['bin0', 'bin1']
  }]
};
inherits(Bin, Transform, {
  transform(_, pulse) {
    const band = _.interval !== false,
      bins = this._bins(_),
      start = bins.start,
      step = bins.step,
      as = _.as || ['bin0', 'bin1'],
      b0 = as[0],
      b1 = as[1];
    let flag;
    if (_.modified()) {
      pulse = pulse.reflow(true);
      flag = pulse.SOURCE;
    } else {
      flag = pulse.modified(accessorFields(_.field)) ? pulse.ADD_MOD : pulse.ADD;
    }
    pulse.visit(flag, band ? t => {
      const v = bins(t);
      // minimum bin value (inclusive)
      t[b0] = v;
      // maximum bin value (exclusive)
      // use convoluted math for better floating point agreement
      // see https://github.com/vega/vega/issues/830
      // infinite values propagate through this formula! #2227
      t[b1] = v == null ? null : start + step * (1 + (v - start) / step);
    } : t => t[b0] = bins(t));
    return pulse.modifies(band ? as : b0);
  },
  _bins(_) {
    if (this.value && !_.modified()) {
      return this.value;
    }
    const field = _.field,
      bins = bin(_),
      step = bins.step;
    let start = bins.start,
      stop = start + Math.ceil((bins.stop - start) / step) * step,
      a,
      d;
    if ((a = _.anchor) != null) {
      d = a - (start + step * Math.floor((a - start) / step));
      start += d;
      stop += d;
    }
    const f = function (t) {
      let v = toNumber(field(t));
      return v == null ? null : v < start ? -Infinity : v > stop ? +Infinity : (v = Math.max(start, Math.min(v, stop - step)), start + step * Math.floor(EPSILON$1 + (v - start) / step));
    };
    f.start = start;
    f.stop = bins.stop;
    f.step = step;
    return this.value = accessor(f, accessorFields(field), _.name || 'bin_' + accessorName(field));
  }
});

function SortedList (idFunc, source, input) {
  const $ = idFunc;
  let data = source || [],
    add = input || [],
    rem = {},
    cnt = 0;
  return {
    add: t => add.push(t),
    remove: t => rem[$(t)] = ++cnt,
    size: () => data.length,
    data: (compare, resort) => {
      if (cnt) {
        data = data.filter(t => !rem[$(t)]);
        rem = {};
        cnt = 0;
      }
      if (resort && compare) {
        data.sort(compare);
      }
      if (add.length) {
        data = compare ? merge$2(compare, data, add.sort(compare)) : data.concat(add);
        add = [];
      }
      return data;
    }
  };
}

/**
 * Collects all data tuples that pass through this operator.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(*,*): number} [params.sort] - An optional
 *   comparator function for additionally sorting the collected tuples.
 */
function Collect$1(params) {
  Transform.call(this, [], params);
}
Collect$1.Definition = {
  'type': 'Collect',
  'metadata': {
    'source': true
  },
  'params': [{
    'name': 'sort',
    'type': 'compare'
  }]
};
inherits(Collect$1, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.ALL),
      list = SortedList(tupleid, this.value, out.materialize(out.ADD).add),
      sort = _.sort,
      mod = pulse.changed() || sort && (_.modified('sort') || pulse.modified(sort.fields));
    out.visit(out.REM, list.remove);
    this.modified(mod);
    this.value = out.source = list.data(stableCompare(sort), mod);

    // propagate tree root if defined
    if (pulse.source && pulse.source.root) {
      this.value.root = pulse.source.root;
    }
    return out;
  }
});

/**
 * Generates a comparator function.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<string|function>} params.fields - The fields to compare.
 * @param {Array<string>} [params.orders] - The sort orders.
 *   Each entry should be one of "ascending" (default) or "descending".
 */
function Compare$1(params) {
  Operator.call(this, null, update$5, params);
}
inherits(Compare$1, Operator);
function update$5(_) {
  return this.value && !_.modified() ? this.value : compare$2(_.fields, _.orders);
}

/**
 * Count regexp-defined pattern occurrences in a text field.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - An accessor for the text field.
 * @param {string} [params.pattern] - RegExp string defining the text pattern.
 * @param {string} [params.case] - One of 'lower', 'upper' or null (mixed) case.
 * @param {string} [params.stopwords] - RegExp string of words to ignore.
 */
function CountPattern(params) {
  Transform.call(this, null, params);
}
CountPattern.Definition = {
  'type': 'CountPattern',
  'metadata': {
    'generates': true,
    'changes': true
  },
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'case',
    'type': 'enum',
    'values': ['upper', 'lower', 'mixed'],
    'default': 'mixed'
  }, {
    'name': 'pattern',
    'type': 'string',
    'default': '[\\w"]+'
  }, {
    'name': 'stopwords',
    'type': 'string',
    'default': ''
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': ['text', 'count']
  }]
};
function tokenize(text, tcase, match) {
  switch (tcase) {
    case 'upper':
      text = text.toUpperCase();
      break;
    case 'lower':
      text = text.toLowerCase();
      break;
  }
  return text.match(match);
}
inherits(CountPattern, Transform, {
  transform(_, pulse) {
    const process = update => tuple => {
      var tokens = tokenize(get(tuple), _.case, match) || [],
        t;
      for (var i = 0, n = tokens.length; i < n; ++i) {
        if (!stop.test(t = tokens[i])) update(t);
      }
    };
    const init = this._parameterCheck(_, pulse),
      counts = this._counts,
      match = this._match,
      stop = this._stop,
      get = _.field,
      as = _.as || ['text', 'count'],
      add = process(t => counts[t] = 1 + (counts[t] || 0)),
      rem = process(t => counts[t] -= 1);
    if (init) {
      pulse.visit(pulse.SOURCE, add);
    } else {
      pulse.visit(pulse.ADD, add);
      pulse.visit(pulse.REM, rem);
    }
    return this._finish(pulse, as); // generate output tuples
  },
  _parameterCheck(_, pulse) {
    let init = false;
    if (_.modified('stopwords') || !this._stop) {
      this._stop = new RegExp('^' + (_.stopwords || '') + '$', 'i');
      init = true;
    }
    if (_.modified('pattern') || !this._match) {
      this._match = new RegExp(_.pattern || '[\\w\']+', 'g');
      init = true;
    }
    if (_.modified('field') || pulse.modified(_.field.fields)) {
      init = true;
    }
    if (init) this._counts = {};
    return init;
  },
  _finish(pulse, as) {
    const counts = this._counts,
      tuples = this._tuples || (this._tuples = {}),
      text = as[0],
      count = as[1],
      out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
    let w, t, c;
    for (w in counts) {
      t = tuples[w];
      c = counts[w] || 0;
      if (!t && c) {
        tuples[w] = t = ingest$1({});
        t[text] = w;
        t[count] = c;
        out.add.push(t);
      } else if (c === 0) {
        if (t) out.rem.push(t);
        counts[w] = null;
        tuples[w] = null;
      } else if (t[count] !== c) {
        t[count] = c;
        out.mod.push(t);
      }
    }
    return out.modifies(as);
  }
});

/**
 * Perform a cross-product of a tuple stream with itself.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object):boolean} [params.filter] - An optional filter
 *   function for selectively including tuples in the cross product.
 * @param {Array<string>} [params.as] - The names of the output fields.
 */
function Cross(params) {
  Transform.call(this, null, params);
}
Cross.Definition = {
  'type': 'Cross',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'filter',
    'type': 'expr'
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': ['a', 'b']
  }]
};
inherits(Cross, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE),
      as = _.as || ['a', 'b'],
      a = as[0],
      b = as[1],
      reset = !this.value || pulse.changed(pulse.ADD_REM) || _.modified('as') || _.modified('filter');
    let data = this.value;
    if (reset) {
      if (data) out.rem = data;
      data = pulse.materialize(pulse.SOURCE).source;
      out.add = this.value = cross(data, a, b, _.filter || truthy);
    } else {
      out.mod = data;
    }
    out.source = this.value;
    return out.modifies(as);
  }
});
function cross(input, a, b, filter) {
  var data = [],
    t = {},
    n = input.length,
    i = 0,
    j,
    left;
  for (; i < n; ++i) {
    t[a] = left = input[i];
    for (j = 0; j < n; ++j) {
      t[b] = input[j];
      if (filter(t)) {
        data.push(ingest$1(t));
        t = {};
        t[a] = left;
      }
    }
  }
  return data;
}

const Distributions = {
  kde: kde,
  mixture: mixture$1,
  normal: gaussian,
  lognormal: lognormal,
  uniform: uniform
};
const DISTRIBUTIONS = 'distributions',
  FUNCTION = 'function',
  FIELD = 'field';

/**
 * Parse a parameter object for a probability distribution.
 * @param {object} def - The distribution parameter object.
 * @param {function():Array<object>} - A method for requesting
 *   source data. Used for distributions (such as KDE) that
 *   require sample data points. This method will only be
 *   invoked if the 'from' parameter for a target data source
 *   is not provided. Typically this method returns backing
 *   source data for a Pulse object.
 * @return {object} - The output distribution object.
 */
function parse$4(def, data) {
  const func = def[FUNCTION];
  if (!has$1(Distributions, func)) {
    error$1('Unknown distribution function: ' + func);
  }
  const d = Distributions[func]();
  for (const name in def) {
    // if data field, extract values
    if (name === FIELD) {
      d.data((def.from || data()).map(def[name]));
    }

    // if distribution mixture, recurse to parse each definition
    else if (name === DISTRIBUTIONS) {
      d[name](def[name].map(_ => parse$4(_, data)));
    }

    // otherwise, simply set the parameter
    else if (typeof d[name] === FUNCTION) {
      d[name](def[name]);
    }
  }
  return d;
}

/**
 * Grid sample points for a probability density. Given a distribution and
 * a sampling extent, will generate points suitable for plotting either
 * PDF (probability density function) or CDF (cumulative distribution
 * function) curves.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {object} params.distribution - The probability distribution. This
 *   is an object parameter dependent on the distribution type.
 * @param {string} [params.method='pdf'] - The distribution method to sample.
 *   One of 'pdf' or 'cdf'.
 * @param {Array<number>} [params.extent] - The [min, max] extent over which
 *   to sample the distribution. This argument is required in most cases, but
 *   can be omitted if the distribution (e.g., 'kde') supports a 'data' method
 *   that returns numerical sample points from which the extent can be deduced.
 * @param {number} [params.minsteps=25] - The minimum number of curve samples
 *   for plotting the density.
 * @param {number} [params.maxsteps=200] - The maximum number of curve samples
 *   for plotting the density.
 * @param {number} [params.steps] - The exact number of curve samples for
 *   plotting the density. If specified, overrides both minsteps and maxsteps
 *   to set an exact number of uniform samples. Useful in conjunction with
 *   a fixed extent to ensure consistent sample points for stacked densities.
 */
function Density(params) {
  Transform.call(this, null, params);
}
const distributions = [{
  'key': {
    'function': 'normal'
  },
  'params': [{
    'name': 'mean',
    'type': 'number',
    'default': 0
  }, {
    'name': 'stdev',
    'type': 'number',
    'default': 1
  }]
}, {
  'key': {
    'function': 'lognormal'
  },
  'params': [{
    'name': 'mean',
    'type': 'number',
    'default': 0
  }, {
    'name': 'stdev',
    'type': 'number',
    'default': 1
  }]
}, {
  'key': {
    'function': 'uniform'
  },
  'params': [{
    'name': 'min',
    'type': 'number',
    'default': 0
  }, {
    'name': 'max',
    'type': 'number',
    'default': 1
  }]
}, {
  'key': {
    'function': 'kde'
  },
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'from',
    'type': 'data'
  }, {
    'name': 'bandwidth',
    'type': 'number',
    'default': 0
  }]
}];
const mixture = {
  'key': {
    'function': 'mixture'
  },
  'params': [{
    'name': 'distributions',
    'type': 'param',
    'array': true,
    'params': distributions
  }, {
    'name': 'weights',
    'type': 'number',
    'array': true
  }]
};
Density.Definition = {
  'type': 'Density',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'extent',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'steps',
    'type': 'number'
  }, {
    'name': 'minsteps',
    'type': 'number',
    'default': 25
  }, {
    'name': 'maxsteps',
    'type': 'number',
    'default': 200
  }, {
    'name': 'method',
    'type': 'string',
    'default': 'pdf',
    'values': ['pdf', 'cdf']
  }, {
    'name': 'distribution',
    'type': 'param',
    'params': distributions.concat(mixture)
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'default': ['value', 'density']
  }]
};
inherits(Density, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
    if (!this.value || pulse.changed() || _.modified()) {
      const dist = parse$4(_.distribution, source$1(pulse)),
        minsteps = _.steps || _.minsteps || 25,
        maxsteps = _.steps || _.maxsteps || 200;
      let method = _.method || 'pdf';
      if (method !== 'pdf' && method !== 'cdf') {
        error$1('Invalid density method: ' + method);
      }
      if (!_.extent && !dist.data) {
        error$1('Missing density extent parameter.');
      }
      method = dist[method];
      const as = _.as || ['value', 'density'],
        domain = _.extent || extent(dist.data()),
        values = sampleCurve(method, domain, minsteps, maxsteps).map(v => {
          const tuple = {};
          tuple[as[0]] = v[0];
          tuple[as[1]] = v[1];
          return ingest$1(tuple);
        });
      if (this.value) out.rem = this.value;
      this.value = out.add = out.source = values;
    }
    return out;
  }
});
function source$1(pulse) {
  return () => pulse.materialize(pulse.SOURCE).source;
}

// use either provided alias or accessor field name
function fieldNames(fields, as) {
  if (!fields) return null;
  return fields.map((f, i) => as[i] || accessorName(f));
}
function partition$1$1(data, groupby, field) {
  const groups = [],
    get = f => f(t);
  let map, i, n, t, k, g;

  // partition data points into groups
  if (groupby == null) {
    groups.push(data.map(field));
  } else {
    for (map = {}, i = 0, n = data.length; i < n; ++i) {
      t = data[i];
      k = groupby.map(get);
      g = map[k];
      if (!g) {
        map[k] = g = [];
        g.dims = k;
        groups.push(g);
      }
      g.push(field(t));
    }
  }
  return groups;
}

const Output$5 = 'bin';

/**
 * Dot density binning for dot plot construction.
 * Based on Leland Wilkinson, Dot Plots, The American Statistician, 1999.
 * https://www.cs.uic.edu/~wilkinson/Publications/dotplots.pdf
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to bin.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors to groupby.
 * @param {number} [params.step] - The step size (bin width) within which dots should be
 *   stacked. Defaults to 1/30 of the extent of the data *field*.
 * @param {boolean} [params.smooth=false] - A boolean flag indicating if dot density
 *   stacks should be smoothed to reduce variance.
 */
function DotBin(params) {
  Transform.call(this, null, params);
}
DotBin.Definition = {
  'type': 'DotBin',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'step',
    'type': 'number'
  }, {
    'name': 'smooth',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'as',
    'type': 'string',
    'default': Output$5
  }]
};
const autostep = (data, field) => span(extent(data, field)) / 30;
inherits(DotBin, Transform, {
  transform(_, pulse) {
    if (this.value && !(_.modified() || pulse.changed())) {
      return pulse; // early exit
    }
    const source = pulse.materialize(pulse.SOURCE).source,
      groups = partition$1$1(pulse.source, _.groupby, identity),
      smooth = _.smooth || false,
      field = _.field,
      step = _.step || autostep(source, field),
      sort = stableCompare((a, b) => field(a) - field(b)),
      as = _.as || Output$5,
      n = groups.length;

    // compute dotplot bins per group
    let min = Infinity,
      max = -Infinity,
      i = 0,
      j;
    for (; i < n; ++i) {
      const g = groups[i].sort(sort);
      j = -1;
      for (const v of dotbin(g, step, smooth, field)) {
        if (v < min) min = v;
        if (v > max) max = v;
        g[++j][as] = v;
      }
    }
    this.value = {
      start: min,
      stop: max,
      step: step
    };
    return pulse.reflow(true).modifies(as);
  }
});

/**
 * Wraps an expression function with access to external parameters.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function} params.expr - The expression function. The
 *  function should accept both a datum and a parameter object.
 *  This operator's value will be a new function that wraps the
 *  expression function with access to this operator's parameters.
 */
function Expression$1(params) {
  Operator.call(this, null, update$4, params);
  this.modified(true);
}
inherits(Expression$1, Operator);
function update$4(_) {
  const expr = _.expr;
  return this.value && !_.modified('expr') ? this.value : accessor(datum => expr(datum, _), accessorFields(expr), accessorName(expr));
}

/**
 * Computes extents (min/max) for a data field.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The field over which to compute extends.
 */
function Extent(params) {
  Transform.call(this, [undefined, undefined], params);
}
Extent.Definition = {
  'type': 'Extent',
  'metadata': {},
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }]
};
inherits(Extent, Transform, {
  transform(_, pulse) {
    const extent = this.value,
      field = _.field,
      mod = pulse.changed() || pulse.modified(field.fields) || _.modified('field');
    let min = extent[0],
      max = extent[1];
    if (mod || min == null) {
      min = +Infinity;
      max = -Infinity;
    }
    pulse.visit(mod ? pulse.SOURCE : pulse.ADD, t => {
      const v = toNumber(field(t));
      if (v != null) {
        // NaNs will fail all comparisons!
        if (v < min) min = v;
        if (v > max) max = v;
      }
    });
    if (!Number.isFinite(min) || !Number.isFinite(max)) {
      let name = accessorName(field);
      if (name) name = ` for field "${name}"`;
      pulse.dataflow.warn(`Infinite extent${name}: [${min}, ${max}]`);
      min = max = undefined;
    }
    this.value = [min, max];
  }
});

/**
 * Provides a bridge between a parent transform and a target subflow that
 * consumes only a subset of the tuples that pass through the parent.
 * @constructor
 * @param {Pulse} pulse - A pulse to use as the value of this operator.
 * @param {Transform} parent - The parent transform (typically a Facet instance).
 */
function Subflow(pulse, parent) {
  Operator.call(this, pulse);
  this.parent = parent;
  this.count = 0;
}
inherits(Subflow, Operator, {
  /**
   * Routes pulses from this subflow to a target transform.
   * @param {Transform} target - A transform that receives the subflow of tuples.
   */
  connect(target) {
    this.detachSubflow = target.detachSubflow;
    this.targets().add(target);
    return target.source = this;
  },
  /**
   * Add an 'add' tuple to the subflow pulse.
   * @param {Tuple} t - The tuple being added.
   */
  add(t) {
    this.count += 1;
    this.value.add.push(t);
  },
  /**
   * Add a 'rem' tuple to the subflow pulse.
   * @param {Tuple} t - The tuple being removed.
   */
  rem(t) {
    this.count -= 1;
    this.value.rem.push(t);
  },
  /**
   * Add a 'mod' tuple to the subflow pulse.
   * @param {Tuple} t - The tuple being modified.
   */
  mod(t) {
    this.value.mod.push(t);
  },
  /**
   * Re-initialize this operator's pulse value.
   * @param {Pulse} pulse - The pulse to copy from.
   * @see Pulse.init
   */
  init(pulse) {
    this.value.init(pulse, pulse.NO_SOURCE);
  },
  /**
   * Evaluate this operator. This method overrides the
   * default behavior to simply return the contained pulse value.
   * @return {Pulse}
   */
  evaluate() {
    // assert: this.value.stamp === pulse.stamp
    return this.value;
  }
});

/**
 * Facets a dataflow into a set of subflows based on a key.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(Dataflow, string): Operator} params.subflow - A function
 *   that generates a subflow of operators and returns its root operator.
 * @param {function(object): *} params.key - The key field to facet by.
 */
function Facet$1(params) {
  Transform.call(this, {}, params);
  this._keys = fastmap(); // cache previously calculated key values

  // keep track of active subflows, use as targets array for listeners
  // this allows us to limit propagation to only updated subflows
  const a = this._targets = [];
  a.active = 0;
  a.forEach = f => {
    for (let i = 0, n = a.active; i < n; ++i) {
      f(a[i], i, a);
    }
  };
}
inherits(Facet$1, Transform, {
  activate(flow) {
    this._targets[this._targets.active++] = flow;
  },
  // parent argument provided by PreFacet subclass
  subflow(key, flow, pulse, parent) {
    const flows = this.value;
    let sf = has$1(flows, key) && flows[key],
      df,
      p;
    if (!sf) {
      p = parent || (p = this._group[key]) && p.tuple;
      df = pulse.dataflow;
      sf = new Subflow(pulse.fork(pulse.NO_SOURCE), this);
      df.add(sf).connect(flow(df, key, p));
      flows[key] = sf;
      this.activate(sf);
    } else if (sf.value.stamp < pulse.stamp) {
      sf.init(pulse);
      this.activate(sf);
    }
    return sf;
  },
  clean() {
    const flows = this.value;
    let detached = 0;
    for (const key in flows) {
      if (flows[key].count === 0) {
        const detach = flows[key].detachSubflow;
        if (detach) detach();
        delete flows[key];
        ++detached;
      }
    }

    // remove inactive targets from the active targets array
    if (detached) {
      const active = this._targets.filter(sf => sf && sf.count > 0);
      this.initTargets(active);
    }
  },
  initTargets(act) {
    const a = this._targets,
      n = a.length,
      m = act ? act.length : 0;
    let i = 0;
    for (; i < m; ++i) {
      a[i] = act[i];
    }
    for (; i < n && a[i] != null; ++i) {
      a[i] = null; // ensure old flows can be garbage collected
    }
    a.active = m;
  },
  transform(_, pulse) {
    const df = pulse.dataflow,
      key = _.key,
      flow = _.subflow,
      cache = this._keys,
      rekey = _.modified('key'),
      subflow = key => this.subflow(key, flow, pulse);
    this._group = _.group || {};
    this.initTargets(); // reset list of active subflows

    pulse.visit(pulse.REM, t => {
      const id = tupleid(t),
        k = cache.get(id);
      if (k !== undefined) {
        cache.delete(id);
        subflow(k).rem(t);
      }
    });
    pulse.visit(pulse.ADD, t => {
      const k = key(t);
      cache.set(tupleid(t), k);
      subflow(k).add(t);
    });
    if (rekey || pulse.modified(key.fields)) {
      pulse.visit(pulse.MOD, t => {
        const id = tupleid(t),
          k0 = cache.get(id),
          k1 = key(t);
        if (k0 === k1) {
          subflow(k1).mod(t);
        } else {
          cache.set(id, k1);
          subflow(k0).rem(t);
          subflow(k1).add(t);
        }
      });
    } else if (pulse.changed(pulse.MOD)) {
      pulse.visit(pulse.MOD, t => {
        subflow(cache.get(tupleid(t))).mod(t);
      });
    }
    if (rekey) {
      pulse.visit(pulse.REFLOW, t => {
        const id = tupleid(t),
          k0 = cache.get(id),
          k1 = key(t);
        if (k0 !== k1) {
          cache.set(id, k1);
          subflow(k0).rem(t);
          subflow(k1).add(t);
        }
      });
    }
    if (pulse.clean()) {
      df.runAfter(() => {
        this.clean();
        cache.clean();
      });
    } else if (cache.empty > df.cleanThreshold) {
      df.runAfter(cache.clean);
    }
    return pulse;
  }
});

/**
 * Generates one or more field accessor functions.
 * If the 'name' parameter is an array, an array of field accessors
 * will be created and the 'as' parameter will be ignored.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {string} params.name - The field name(s) to access.
 * @param {string} params.as - The accessor function name.
 */
function Field$1(params) {
  Operator.call(this, null, update$3, params);
}
inherits(Field$1, Operator);
function update$3(_) {
  return this.value && !_.modified() ? this.value : isArray(_.name) ? array$2(_.name).map(f => field$1(f)) : field$1(_.name, _.as);
}

/**
 * Filters data tuples according to a predicate function.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.expr - The predicate expression function
 *   that determines a tuple's filter status. Truthy values pass the filter.
 */
function Filter(params) {
  Transform.call(this, fastmap(), params);
}
Filter.Definition = {
  'type': 'Filter',
  'metadata': {
    'changes': true
  },
  'params': [{
    'name': 'expr',
    'type': 'expr',
    'required': true
  }]
};
inherits(Filter, Transform, {
  transform(_, pulse) {
    const df = pulse.dataflow,
      cache = this.value,
      // cache ids of filtered tuples
      output = pulse.fork(),
      add = output.add,
      rem = output.rem,
      mod = output.mod,
      test = _.expr;
    let isMod = true;
    pulse.visit(pulse.REM, t => {
      const id = tupleid(t);
      if (!cache.has(id)) rem.push(t);else cache.delete(id);
    });
    pulse.visit(pulse.ADD, t => {
      if (test(t, _)) add.push(t);else cache.set(tupleid(t), 1);
    });
    function revisit(t) {
      const id = tupleid(t),
        b = test(t, _),
        s = cache.get(id);
      if (b && s) {
        cache.delete(id);
        add.push(t);
      } else if (!b && !s) {
        cache.set(id, 1);
        rem.push(t);
      } else if (isMod && b && !s) {
        mod.push(t);
      }
    }
    pulse.visit(pulse.MOD, revisit);
    if (_.modified()) {
      isMod = false;
      pulse.visit(pulse.REFLOW, revisit);
    }
    if (cache.empty > df.cleanThreshold) df.runAfter(cache.clean);
    return output;
  }
});

/**
 * Flattens array-typed field values into new data objects.
 * If multiple fields are specified, they are treated as parallel arrays,
 * with output values included for each matching index (or null if missing).
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *>} params.fields - An array of field
 *   accessors for the tuple fields that should be flattened.
 * @param {string} [params.index] - Optional output field name for index
 *   value. If unspecified, no index field is included in the output.
 * @param {Array<string>} [params.as] - Output field names for flattened
 *   array fields. Any unspecified fields will use the field name provided
 *   by the fields accessors.
 */
function Flatten(params) {
  Transform.call(this, [], params);
}
Flatten.Definition = {
  'type': 'Flatten',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'fields',
    'type': 'field',
    'array': true,
    'required': true
  }, {
    'name': 'index',
    'type': 'string'
  }, {
    'name': 'as',
    'type': 'string',
    'array': true
  }]
};
inherits(Flatten, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE),
      fields = _.fields,
      as = fieldNames(fields, _.as || []),
      index = _.index || null,
      m = as.length;

    // remove any previous results
    out.rem = this.value;

    // generate flattened tuples
    pulse.visit(pulse.SOURCE, t => {
      const arrays = fields.map(f => f(t)),
        maxlen = arrays.reduce((l, a) => Math.max(l, a.length), 0);
      let i = 0,
        j,
        d,
        v;
      for (; i < maxlen; ++i) {
        d = derive(t);
        for (j = 0; j < m; ++j) {
          d[as[j]] = (v = arrays[j][i]) == null ? null : v;
        }
        if (index) {
          d[index] = i;
        }
        out.add.push(d);
      }
    });
    this.value = out.source = out.add;
    if (index) out.modifies(index);
    return out.modifies(as);
  }
});

/**
 * Folds one more tuple fields into multiple tuples in which the field
 * name and values are available under new 'key' and 'value' fields.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.fields - An array of field accessors
 *   for the tuple fields that should be folded.
 * @param {Array<string>} [params.as] - Output field names for folded key
 *   and value fields, defaults to ['key', 'value'].
 */
function Fold(params) {
  Transform.call(this, [], params);
}
Fold.Definition = {
  'type': 'Fold',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'fields',
    'type': 'field',
    'array': true,
    'required': true
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': ['key', 'value']
  }]
};
inherits(Fold, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE),
      fields = _.fields,
      fnames = fields.map(accessorName),
      as = _.as || ['key', 'value'],
      k = as[0],
      v = as[1],
      n = fields.length;
    out.rem = this.value;
    pulse.visit(pulse.SOURCE, t => {
      for (let i = 0, d; i < n; ++i) {
        d = derive(t);
        d[k] = fnames[i];
        d[v] = fields[i](t);
        out.add.push(d);
      }
    });
    this.value = out.source = out.add;
    return out.modifies(as);
  }
});

/**
 * Invokes a function for each data tuple and saves the results as a new field.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.expr - The formula function to invoke for each tuple.
 * @param {string} params.as - The field name under which to save the result.
 * @param {boolean} [params.initonly=false] - If true, the formula is applied to
 *   added tuples only, and does not update in response to modifications.
 */
function Formula(params) {
  Transform.call(this, null, params);
}
Formula.Definition = {
  'type': 'Formula',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'expr',
    'type': 'expr',
    'required': true
  }, {
    'name': 'as',
    'type': 'string',
    'required': true
  }, {
    'name': 'initonly',
    'type': 'boolean'
  }]
};
inherits(Formula, Transform, {
  transform(_, pulse) {
    const func = _.expr,
      as = _.as,
      mod = _.modified(),
      flag = _.initonly ? pulse.ADD : mod ? pulse.SOURCE : pulse.modified(func.fields) || pulse.modified(as) ? pulse.ADD_MOD : pulse.ADD;
    if (mod) {
      // parameters updated, need to reflow
      pulse = pulse.materialize().reflow(true);
    }
    if (!_.initonly) {
      pulse.modifies(as);
    }
    return pulse.visit(flag, t => t[as] = func(t, _));
  }
});

/**
 * Generates data tuples using a provided generator function.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(Parameters): object} params.generator - A tuple generator
 *   function. This function is given the operator parameters as input.
 *   Changes to any additional parameters will not trigger re-calculation
 *   of previously generated tuples. Only future tuples are affected.
 * @param {number} params.size - The number of tuples to produce.
 */
function Generate(params) {
  Transform.call(this, [], params);
}
inherits(Generate, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.ALL),
      gen = _.generator;
    let data = this.value,
      num = _.size - data.length,
      add,
      rem,
      t;
    if (num > 0) {
      // need more tuples, generate and add
      for (add = []; --num >= 0;) {
        add.push(t = ingest$1(gen(_)));
        data.push(t);
      }
      out.add = out.add.length ? out.materialize(out.ADD).add.concat(add) : add;
    } else {
      // need fewer tuples, remove
      rem = data.slice(0, -num);
      out.rem = out.rem.length ? out.materialize(out.REM).rem.concat(rem) : rem;
      data = data.slice(-num);
    }
    out.source = this.value = data;
    return out;
  }
});

const Methods$1 = {
  value: 'value',
  median: median,
  mean: mean,
  min: min$1,
  max: max$1
};
const Empty$1 = [];

/**
 * Impute missing values.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to impute.
 * @param {Array<function(object): *>} [params.groupby] - An array of
 *   accessors to determine series within which to perform imputation.
 * @param {function(object): *} params.key - An accessor for a key value.
 *   Each key value should be unique within a group. New tuples will be
 *   imputed for any key values that are not found within a group.
 * @param {Array<*>} [params.keyvals] - Optional array of required key
 *   values. New tuples will be imputed for any key values that are not
 *   found within a group. In addition, these values will be automatically
 *   augmented with the key values observed in the input data.
 * @param {string} [method='value'] - The imputation method to use. One of
 *   'value', 'mean', 'median', 'max', 'min'.
 * @param {*} [value=0] - The constant value to use for imputation
 *   when using method 'value'.
 */
function Impute(params) {
  Transform.call(this, [], params);
}
Impute.Definition = {
  'type': 'Impute',
  'metadata': {
    'changes': true
  },
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'key',
    'type': 'field',
    'required': true
  }, {
    'name': 'keyvals',
    'array': true
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'method',
    'type': 'enum',
    'default': 'value',
    'values': ['value', 'mean', 'median', 'max', 'min']
  }, {
    'name': 'value',
    'default': 0
  }]
};
function getValue(_) {
  var m = _.method || Methods$1.value,
    v;
  if (Methods$1[m] == null) {
    error$1('Unrecognized imputation method: ' + m);
  } else if (m === Methods$1.value) {
    v = _.value !== undefined ? _.value : 0;
    return () => v;
  } else {
    return Methods$1[m];
  }
}
function getField$1(_) {
  const f = _.field;
  return t => t ? f(t) : NaN;
}
inherits(Impute, Transform, {
  transform(_, pulse) {
    var out = pulse.fork(pulse.ALL),
      impute = getValue(_),
      field = getField$1(_),
      fName = accessorName(_.field),
      kName = accessorName(_.key),
      gNames = (_.groupby || []).map(accessorName),
      groups = partition$3(pulse.source, _.groupby, _.key, _.keyvals),
      curr = [],
      prev = this.value,
      m = groups.domain.length,
      group,
      value,
      gVals,
      kVal,
      g,
      i,
      j,
      l,
      n,
      t;
    for (g = 0, l = groups.length; g < l; ++g) {
      group = groups[g];
      gVals = group.values;
      value = NaN;

      // add tuples for missing values
      for (j = 0; j < m; ++j) {
        if (group[j] != null) continue;
        kVal = groups.domain[j];
        t = {
          _impute: true
        };
        for (i = 0, n = gVals.length; i < n; ++i) t[gNames[i]] = gVals[i];
        t[kName] = kVal;
        t[fName] = Number.isNaN(value) ? value = impute(group, field) : value;
        curr.push(ingest$1(t));
      }
    }

    // update pulse with imputed tuples
    if (curr.length) out.add = out.materialize(out.ADD).add.concat(curr);
    if (prev.length) out.rem = out.materialize(out.REM).rem.concat(prev);
    this.value = curr;
    return out;
  }
});
function partition$3(data, groupby, key, keyvals) {
  var get = f => f(t),
    groups = [],
    domain = keyvals ? keyvals.slice() : [],
    kMap = {},
    gMap = {},
    gVals,
    gKey,
    group,
    i,
    j,
    k,
    n,
    t;
  domain.forEach((k, i) => kMap[k] = i + 1);
  for (i = 0, n = data.length; i < n; ++i) {
    t = data[i];
    k = key(t);
    j = kMap[k] || (kMap[k] = domain.push(k));
    gKey = (gVals = groupby ? groupby.map(get) : Empty$1) + '';
    if (!(group = gMap[gKey])) {
      group = gMap[gKey] = [];
      groups.push(group);
      group.values = gVals;
    }
    group[j - 1] = t;
  }
  groups.domain = domain;
  return groups;
}

/**
 * Extend input tuples with aggregate values.
 * Calcuates aggregate values and joins them with the input stream.
 * @constructor
 */
function JoinAggregate(params) {
  Aggregate$1.call(this, params);
}
JoinAggregate.Definition = {
  'type': 'JoinAggregate',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'fields',
    'type': 'field',
    'null': true,
    'array': true
  }, {
    'name': 'ops',
    'type': 'enum',
    'array': true,
    'values': ValidAggregateOps
  }, {
    'name': 'as',
    'type': 'string',
    'null': true,
    'array': true
  }, {
    'name': 'key',
    'type': 'field'
  }]
};
inherits(JoinAggregate, Aggregate$1, {
  transform(_, pulse) {
    const aggr = this,
      mod = _.modified();
    let cells;

    // process all input tuples to calculate aggregates
    if (aggr.value && (mod || pulse.modified(aggr._inputs, true))) {
      cells = aggr.value = mod ? aggr.init(_) : {};
      pulse.visit(pulse.SOURCE, t => aggr.add(t));
    } else {
      cells = aggr.value = aggr.value || this.init(_);
      pulse.visit(pulse.REM, t => aggr.rem(t));
      pulse.visit(pulse.ADD, t => aggr.add(t));
    }

    // update aggregation cells
    aggr.changes();

    // write aggregate values to input tuples
    pulse.visit(pulse.SOURCE, t => {
      extend(t, cells[aggr.cellkey(t)].tuple);
    });
    return pulse.reflow(mod).modifies(this._outputs);
  },
  changes() {
    const adds = this._adds,
      mods = this._mods;
    let i, n;
    for (i = 0, n = this._alen; i < n; ++i) {
      this.celltuple(adds[i]);
      adds[i] = null; // for garbage collection
    }
    for (i = 0, n = this._mlen; i < n; ++i) {
      this.celltuple(mods[i]);
      mods[i] = null; // for garbage collection
    }
    this._alen = this._mlen = 0; // reset list of active cells
  }
});

/**
 * Compute kernel density estimates (KDE) for one or more data groups.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors
 *   to groupby.
 * @param {function(object): *} params.field - An accessor for the data field
 *   to estimate.
 * @param {number} [params.bandwidth=0] - The KDE kernel bandwidth.
 *   If zero or unspecified, the bandwidth is automatically determined.
 * @param {boolean} [params.counts=false] - A boolean flag indicating if the
 *   output values should be probability estimates (false, default) or
 *   smoothed counts (true).
 * @param {string} [params.cumulative=false] - A boolean flag indicating if a
 *   density (false) or cumulative distribution (true) should be generated.
 * @param {Array<number>} [params.extent] - The domain extent over which to
 *   plot the density. If unspecified, the [min, max] data extent is used.
 * @param {string} [params.resolve='independent'] - Indicates how parameters for
 *   multiple densities should be resolved. If "independent" (the default), each
 *   density may have its own domain extent and dynamic number of curve sample
 *   steps. If "shared", the KDE transform will ensure that all densities are
 *   defined over a shared domain and curve steps, enabling stacking.
 * @param {number} [params.minsteps=25] - The minimum number of curve samples
 *   for plotting the density.
 * @param {number} [params.maxsteps=200] - The maximum number of curve samples
 *   for plotting the density.
 * @param {number} [params.steps] - The exact number of curve samples for
 *   plotting the density. If specified, overrides both minsteps and maxsteps
 *   to set an exact number of uniform samples. Useful in conjunction with
 *   a fixed extent to ensure consistent sample points for stacked densities.
 */
function KDE(params) {
  Transform.call(this, null, params);
}
KDE.Definition = {
  'type': 'KDE',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'cumulative',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'counts',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'bandwidth',
    'type': 'number',
    'default': 0
  }, {
    'name': 'extent',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'resolve',
    'type': 'enum',
    'values': ['shared', 'independent'],
    'default': 'independent'
  }, {
    'name': 'steps',
    'type': 'number'
  }, {
    'name': 'minsteps',
    'type': 'number',
    'default': 25
  }, {
    'name': 'maxsteps',
    'type': 'number',
    'default': 200
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'default': ['value', 'density']
  }]
};
inherits(KDE, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
    if (!this.value || pulse.changed() || _.modified()) {
      const source = pulse.materialize(pulse.SOURCE).source,
        groups = partition$1$1(source, _.groupby, _.field),
        names = (_.groupby || []).map(accessorName),
        bandwidth = _.bandwidth,
        method = _.cumulative ? 'cdf' : 'pdf',
        as = _.as || ['value', 'density'],
        values = [];
      let domain = _.extent,
        minsteps = _.steps || _.minsteps || 25,
        maxsteps = _.steps || _.maxsteps || 200;
      if (method !== 'pdf' && method !== 'cdf') {
        error$1('Invalid density method: ' + method);
      }
      if (_.resolve === 'shared') {
        if (!domain) domain = extent(source, _.field);
        minsteps = maxsteps = _.steps || maxsteps;
      }
      groups.forEach(g => {
        const density = kde(g, bandwidth)[method],
          scale = _.counts ? g.length : 1,
          local = domain || extent(g);
        sampleCurve(density, local, minsteps, maxsteps).forEach(v => {
          const t = {};
          for (let i = 0; i < names.length; ++i) {
            t[names[i]] = g.dims[i];
          }
          t[as[0]] = v[0];
          t[as[1]] = v[1] * scale;
          values.push(ingest$1(t));
        });
      });
      if (this.value) out.rem = this.value;
      this.value = out.add = out.source = values;
    }
    return out;
  }
});

/**
 * Generates a key function.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<string>} params.fields - The field name(s) for the key function.
 * @param {boolean} params.flat - A boolean flag indicating if the field names
 *  should be treated as flat property names, side-stepping nested field
 *  lookups normally indicated by dot or bracket notation.
 */
function Key$1(params) {
  Operator.call(this, null, update$2, params);
}
inherits(Key$1, Operator);
function update$2(_) {
  return this.value && !_.modified() ? this.value : key(_.fields, _.flat);
}

/**
 * Load and parse data from an external source. Marshalls parameter
 * values and then invokes the Dataflow request method.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {string} params.url - The URL to load from.
 * @param {object} params.format - The data format options.
 */
function Load$1(params) {
  Transform.call(this, [], params);
  this._pending = null;
}
inherits(Load$1, Transform, {
  transform(_, pulse) {
    const df = pulse.dataflow;
    if (this._pending) {
      // update state and return pulse
      return output(this, pulse, this._pending);
    }
    if (stop(_)) return pulse.StopPropagation;
    if (_.values) {
      // parse and ingest values, return output pulse
      return output(this, pulse, df.parse(_.values, _.format));
    } else if (_.async) {
      // return promise for non-blocking async loading
      const p = df.request(_.url, _.format).then(res => {
        this._pending = array$2(res.data);
        return df => df.touch(this);
      });
      return {
        async: p
      };
    } else {
      // return promise for synchronous loading
      return df.request(_.url, _.format).then(res => output(this, pulse, array$2(res.data)));
    }
  }
});
function stop(_) {
  return _.modified('async') && !(_.modified('values') || _.modified('url') || _.modified('format'));
}
function output(op, pulse, data) {
  data.forEach(ingest$1);
  const out = pulse.fork(pulse.NO_FIELDS & pulse.NO_SOURCE);
  out.rem = op.value;
  op.value = out.source = out.add = data;
  op._pending = null;
  if (out.rem.length) out.clean(true);
  return out;
}

/**
 * Extend tuples by joining them with values from a lookup table.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Map} params.index - The lookup table map.
 * @param {Array<function(object): *} params.fields - The fields to lookup.
 * @param {Array<string>} params.as - Output field names for each lookup value.
 * @param {*} [params.default] - A default value to use if lookup fails.
 */
function Lookup(params) {
  Transform.call(this, {}, params);
}
Lookup.Definition = {
  'type': 'Lookup',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'index',
    'type': 'index',
    'params': [{
      'name': 'from',
      'type': 'data',
      'required': true
    }, {
      'name': 'key',
      'type': 'field',
      'required': true
    }]
  }, {
    'name': 'values',
    'type': 'field',
    'array': true
  }, {
    'name': 'fields',
    'type': 'field',
    'array': true,
    'required': true
  }, {
    'name': 'as',
    'type': 'string',
    'array': true
  }, {
    'name': 'default',
    'default': null
  }]
};
inherits(Lookup, Transform, {
  transform(_, pulse) {
    const keys = _.fields,
      index = _.index,
      values = _.values,
      defaultValue = _.default == null ? null : _.default,
      reset = _.modified(),
      n = keys.length;
    let flag = reset ? pulse.SOURCE : pulse.ADD,
      out = pulse,
      as = _.as,
      set,
      m,
      mods;
    if (values) {
      m = values.length;
      if (n > 1 && !as) {
        error$1('Multi-field lookup requires explicit "as" parameter.');
      }
      if (as && as.length !== n * m) {
        error$1('The "as" parameter has too few output field names.');
      }
      as = as || values.map(accessorName);
      set = function (t) {
        for (var i = 0, k = 0, j, v; i < n; ++i) {
          v = index.get(keys[i](t));
          if (v == null) for (j = 0; j < m; ++j, ++k) t[as[k]] = defaultValue;else for (j = 0; j < m; ++j, ++k) t[as[k]] = values[j](v);
        }
      };
    } else {
      if (!as) {
        error$1('Missing output field names.');
      }
      set = function (t) {
        for (var i = 0, v; i < n; ++i) {
          v = index.get(keys[i](t));
          t[as[i]] = v == null ? defaultValue : v;
        }
      };
    }
    if (reset) {
      out = pulse.reflow(true);
    } else {
      mods = keys.some(k => pulse.modified(k.fields));
      flag |= mods ? pulse.MOD : 0;
    }
    pulse.visit(flag, set);
    return out.modifies(as);
  }
});

/**
 * Computes global min/max extents over a collection of extents.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<Array<number>>} params.extents - The input extents.
 */
function MultiExtent$1(params) {
  Operator.call(this, null, update$1, params);
}
inherits(MultiExtent$1, Operator);
function update$1(_) {
  if (this.value && !_.modified()) {
    return this.value;
  }
  const ext = _.extents,
    n = ext.length;
  let min = +Infinity,
    max = -Infinity,
    i,
    e;
  for (i = 0; i < n; ++i) {
    e = ext[i];
    if (e[0] < min) min = e[0];
    if (e[1] > max) max = e[1];
  }
  return [min, max];
}

/**
 * Merge a collection of value arrays.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<Array<*>>} params.values - The input value arrrays.
 */
function MultiValues$1(params) {
  Operator.call(this, null, update, params);
}
inherits(MultiValues$1, Operator);
function update(_) {
  return this.value && !_.modified() ? this.value : _.values.reduce((data, _) => data.concat(_), []);
}

/**
 * Operator whose value is simply its parameter hash. This operator is
 * useful for enabling reactive updates to values of nested objects.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function Params$2(params) {
  Transform.call(this, null, params);
}
inherits(Params$2, Transform, {
  transform(_, pulse) {
    this.modified(_.modified());
    this.value = _;
    return pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS); // do not pass tuples
  }
});

/**
 * Aggregate and pivot selected field values to become new fields.
 * This operator is useful to construction cross-tabulations.
 * @constructor
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors
 *  to groupby. These fields act just like groupby fields of an Aggregate transform.
 * @param {function(object): *} params.field - The field to pivot on. The unique
 *  values of this field become new field names in the output stream.
 * @param {function(object): *} params.value - The field to populate pivoted fields.
 *  The aggregate values of this field become the values of the new pivoted fields.
 * @param {string} [params.op] - The aggregation operation for the value field,
 *  applied per cell in the output stream. The default is "sum".
 * @param {number} [params.limit] - An optional parameter indicating the maximum
 *  number of pivoted fields to generate. The pivoted field names are sorted in
 *  ascending order prior to enforcing the limit.
 */
function Pivot(params) {
  Aggregate$1.call(this, params);
}
Pivot.Definition = {
  'type': 'Pivot',
  'metadata': {
    'generates': true,
    'changes': true
  },
  'params': [{
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'value',
    'type': 'field',
    'required': true
  }, {
    'name': 'op',
    'type': 'enum',
    'values': ValidAggregateOps,
    'default': 'sum'
  }, {
    'name': 'limit',
    'type': 'number',
    'default': 0
  }, {
    'name': 'key',
    'type': 'field'
  }]
};
inherits(Pivot, Aggregate$1, {
  _transform: Aggregate$1.prototype.transform,
  transform(_, pulse) {
    return this._transform(aggregateParams(_, pulse), pulse);
  }
});

// Shoehorn a pivot transform into an aggregate transform!
// First collect all unique pivot field values.
// Then generate aggregate fields for each output pivot field.
function aggregateParams(_, pulse) {
  const key = _.field,
    value = _.value,
    op = (_.op === 'count' ? '__count__' : _.op) || 'sum',
    fields = accessorFields(key).concat(accessorFields(value)),
    keys = pivotKeys(key, _.limit || 0, pulse);

  // if data stream content changes, pivot fields may change
  // flag parameter modification to ensure re-initialization
  if (pulse.changed()) _.set('__pivot__', null, null, true);
  return {
    key: _.key,
    groupby: _.groupby,
    ops: keys.map(() => op),
    fields: keys.map(k => get$3(k, key, value, fields)),
    as: keys.map(k => k + ''),
    modified: _.modified.bind(_)
  };
}

// Generate aggregate field accessor.
// Output NaN for non-existent values; aggregator will ignore!
function get$3(k, key, value, fields) {
  return accessor(d => key(d) === k ? value(d) : NaN, fields, k + '');
}

// Collect (and optionally limit) all unique pivot values.
function pivotKeys(key, limit, pulse) {
  const map = {},
    list = [];
  pulse.visit(pulse.SOURCE, t => {
    const k = key(t);
    if (!map[k]) {
      map[k] = 1;
      list.push(k);
    }
  });
  list.sort(ascending$1);
  return limit ? list.slice(0, limit) : list;
}

/**
 * Partitions pre-faceted data into tuple subflows.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(Dataflow, string): Operator} params.subflow - A function
 *   that generates a subflow of operators and returns its root operator.
 * @param {function(object): Array<object>} params.field - The field
 *   accessor for an array of subflow tuple objects.
 */
function PreFacet$1(params) {
  Facet$1.call(this, params);
}
inherits(PreFacet$1, Facet$1, {
  transform(_, pulse) {
    const flow = _.subflow,
      field = _.field,
      subflow = t => this.subflow(tupleid(t), flow, pulse, t);
    if (_.modified('field') || field && pulse.modified(accessorFields(field))) {
      error$1('PreFacet does not support field modification.');
    }
    this.initTargets(); // reset list of active subflows

    if (field) {
      pulse.visit(pulse.MOD, t => {
        const sf = subflow(t);
        field(t).forEach(_ => sf.mod(_));
      });
      pulse.visit(pulse.ADD, t => {
        const sf = subflow(t);
        field(t).forEach(_ => sf.add(ingest$1(_)));
      });
      pulse.visit(pulse.REM, t => {
        const sf = subflow(t);
        field(t).forEach(_ => sf.rem(_));
      });
    } else {
      pulse.visit(pulse.MOD, t => subflow(t).mod(t));
      pulse.visit(pulse.ADD, t => subflow(t).add(t));
      pulse.visit(pulse.REM, t => subflow(t).rem(t));
    }
    if (pulse.clean()) {
      pulse.runAfter(() => this.clean());
    }
    return pulse;
  }
});

/**
 * Performs a relational projection, copying selected fields from source
 * tuples to a new set of derived tuples.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *} params.fields - The fields to project,
 *   as an array of field accessors. If unspecified, all fields will be
 *   copied with names unchanged.
 * @param {Array<string>} [params.as] - Output field names for each projected
 *   field. Any unspecified fields will use the field name provided by
 *   the field accessor.
 */
function Project(params) {
  Transform.call(this, null, params);
}
Project.Definition = {
  'type': 'Project',
  'metadata': {
    'generates': true,
    'changes': true
  },
  'params': [{
    'name': 'fields',
    'type': 'field',
    'array': true
  }, {
    'name': 'as',
    'type': 'string',
    'null': true,
    'array': true
  }]
};
inherits(Project, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE),
      fields = _.fields,
      as = fieldNames(_.fields, _.as || []),
      derive = fields ? (s, t) => project$1(s, t, fields, as) : rederive;
    let lut;
    if (this.value) {
      lut = this.value;
    } else {
      pulse = pulse.addAll();
      lut = this.value = {};
    }
    pulse.visit(pulse.REM, t => {
      const id = tupleid(t);
      out.rem.push(lut[id]);
      lut[id] = null;
    });
    pulse.visit(pulse.ADD, t => {
      const dt = derive(t, ingest$1({}));
      lut[tupleid(t)] = dt;
      out.add.push(dt);
    });
    pulse.visit(pulse.MOD, t => {
      out.mod.push(derive(t, lut[tupleid(t)]));
    });
    return out;
  }
});
function project$1(s, t, fields, as) {
  for (let i = 0, n = fields.length; i < n; ++i) {
    t[as[i]] = fields[i](s);
  }
  return t;
}

/**
 * Proxy the value of another operator as a pure signal value.
 * Ensures no tuples are propagated.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {*} params.value - The value to proxy, becomes the value of this operator.
 */
function Proxy$1(params) {
  Transform.call(this, null, params);
}
inherits(Proxy$1, Transform, {
  transform(_, pulse) {
    this.value = _.value;
    return _.modified('value') ? pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS) : pulse.StopPropagation;
  }
});

/**
 * Generates sample quantile values from an input data stream.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - An accessor for the data field
 *   over which to calculate quantile values.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors
 *   to groupby.
 * @param {Array<number>} [params.probs] - An array of probabilities in
 *   the range (0, 1) for which to compute quantile values. If not specified,
 *   the *step* parameter will be used.
 * @param {Array<number>} [params.step=0.01] - A probability step size for
 *   sampling quantile values. All values from one-half the step size up to
 *   1 (exclusive) will be sampled. This parameter is only used if the
 *   *quantiles* parameter is not provided.
 */
function Quantile$1(params) {
  Transform.call(this, null, params);
}
Quantile$1.Definition = {
  'type': 'Quantile',
  'metadata': {
    'generates': true,
    'changes': true
  },
  'params': [{
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'probs',
    'type': 'number',
    'array': true
  }, {
    'name': 'step',
    'type': 'number',
    'default': 0.01
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'default': ['prob', 'value']
  }]
};
const EPSILON = 1e-14;
inherits(Quantile$1, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      as = _.as || ['prob', 'value'];
    if (this.value && !_.modified() && !pulse.changed()) {
      out.source = this.value;
      return out;
    }
    const source = pulse.materialize(pulse.SOURCE).source,
      groups = partition$1$1(source, _.groupby, _.field),
      names = (_.groupby || []).map(accessorName),
      values = [],
      step = _.step || 0.01,
      p = _.probs || range$4(step / 2, 1 - EPSILON, step),
      n = p.length;
    groups.forEach(g => {
      const q = quantiles(g, p);
      for (let i = 0; i < n; ++i) {
        const t = {};
        for (let i = 0; i < names.length; ++i) {
          t[names[i]] = g.dims[i];
        }
        t[as[0]] = p[i];
        t[as[1]] = q[i];
        values.push(ingest$1(t));
      }
    });
    if (this.value) out.rem = this.value;
    this.value = out.add = out.source = values;
    return out;
  }
});

/**
 * Relays a data stream between data processing pipelines.
 * If the derive parameter is set, this transform will create derived
 * copies of observed tuples. This provides derived data streams in which
 * modifications to the tuples do not pollute an upstream data source.
 * @param {object} params - The parameters for this operator.
 * @param {number} [params.derive=false] - Boolean flag indicating if
 *   the transform should make derived copies of incoming tuples.
 * @constructor
 */
function Relay$1(params) {
  Transform.call(this, null, params);
}
inherits(Relay$1, Transform, {
  transform(_, pulse) {
    let out, lut;
    if (this.value) {
      lut = this.value;
    } else {
      out = pulse = pulse.addAll();
      lut = this.value = {};
    }
    if (_.derive) {
      out = pulse.fork(pulse.NO_SOURCE);
      pulse.visit(pulse.REM, t => {
        const id = tupleid(t);
        out.rem.push(lut[id]);
        lut[id] = null;
      });
      pulse.visit(pulse.ADD, t => {
        const dt = derive(t);
        lut[tupleid(t)] = dt;
        out.add.push(dt);
      });
      pulse.visit(pulse.MOD, t => {
        const dt = lut[tupleid(t)];
        for (const k in t) {
          dt[k] = t[k];
          // down stream writes may overwrite re-derived tuples
          // conservatively mark all source fields as modified
          out.modifies(k);
        }
        out.mod.push(dt);
      });
    }
    return out;
  }
});

/**
 * Samples tuples passing through this operator.
 * Uses reservoir sampling to maintain a representative sample.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {number} [params.size=1000] - The maximum number of samples.
 */
function Sample(params) {
  Transform.call(this, [], params);
  this.count = 0;
}
Sample.Definition = {
  'type': 'Sample',
  'metadata': {},
  'params': [{
    'name': 'size',
    'type': 'number',
    'default': 1000
  }]
};
inherits(Sample, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE),
      mod = _.modified('size'),
      num = _.size,
      map = this.value.reduce((m, t) => (m[tupleid(t)] = 1, m), {});
    let res = this.value,
      cnt = this.count,
      cap = 0;

    // sample reservoir update function
    function update(t) {
      let p, idx;
      if (res.length < num) {
        res.push(t);
      } else {
        idx = ~~((cnt + 1) * random());
        if (idx < res.length && idx >= cap) {
          p = res[idx];
          if (map[tupleid(p)]) out.rem.push(p); // eviction
          res[idx] = t;
        }
      }
      ++cnt;
    }
    if (pulse.rem.length) {
      // find all tuples that should be removed, add to output
      pulse.visit(pulse.REM, t => {
        const id = tupleid(t);
        if (map[id]) {
          map[id] = -1;
          out.rem.push(t);
        }
        --cnt;
      });

      // filter removed tuples out of the sample reservoir
      res = res.filter(t => map[tupleid(t)] !== -1);
    }
    if ((pulse.rem.length || mod) && res.length < num && pulse.source) {
      // replenish sample if backing data source is available
      cap = cnt = res.length;
      pulse.visit(pulse.SOURCE, t => {
        // update, but skip previously sampled tuples
        if (!map[tupleid(t)]) update(t);
      });
      cap = -1;
    }
    if (mod && res.length > num) {
      const n = res.length - num;
      for (let i = 0; i < n; ++i) {
        map[tupleid(res[i])] = -1;
        out.rem.push(res[i]);
      }
      res = res.slice(n);
    }
    if (pulse.mod.length) {
      // propagate modified tuples in the sample reservoir
      pulse.visit(pulse.MOD, t => {
        if (map[tupleid(t)]) out.mod.push(t);
      });
    }
    if (pulse.add.length) {
      // update sample reservoir
      pulse.visit(pulse.ADD, update);
    }
    if (pulse.add.length || cap < 0) {
      // output newly added tuples
      out.add = res.filter(t => !map[tupleid(t)]);
    }
    this.count = cnt;
    this.value = out.source = res;
    return out;
  }
});

/**
 * Generates data tuples for a specified sequence range of numbers.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {number} params.start - The first number in the sequence.
 * @param {number} params.stop - The last number (exclusive) in the sequence.
 * @param {number} [params.step=1] - The step size between numbers in the sequence.
 */
function Sequence(params) {
  Transform.call(this, null, params);
}
Sequence.Definition = {
  'type': 'Sequence',
  'metadata': {
    'generates': true,
    'changes': true
  },
  'params': [{
    'name': 'start',
    'type': 'number',
    'required': true
  }, {
    'name': 'stop',
    'type': 'number',
    'required': true
  }, {
    'name': 'step',
    'type': 'number',
    'default': 1
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'data'
  }]
};
inherits(Sequence, Transform, {
  transform(_, pulse) {
    if (this.value && !_.modified()) return;
    const out = pulse.materialize().fork(pulse.MOD),
      as = _.as || 'data';
    out.rem = this.value ? pulse.rem.concat(this.value) : pulse.rem;
    this.value = range$4(_.start, _.stop, _.step || 1).map(v => {
      const t = {};
      t[as] = v;
      return ingest$1(t);
    });
    out.add = pulse.add.concat(this.value);
    return out;
  }
});

/**
 * Propagates a new pulse without any tuples so long as the input
 * pulse contains some added, removed or modified tuples.
 * @param {object} params - The parameters for this operator.
 * @constructor
 */
function Sieve$1(params) {
  Transform.call(this, null, params);
  this.modified(true); // always treat as modified
}
inherits(Sieve$1, Transform, {
  transform(_, pulse) {
    this.value = pulse.source;
    return pulse.changed() ? pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS) : pulse.StopPropagation;
  }
});

/**
 * Discretize dates to specific time units.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The data field containing date/time values.
 */
function TimeUnit(params) {
  Transform.call(this, null, params);
}
const OUTPUT = ['unit0', 'unit1'];
TimeUnit.Definition = {
  'type': 'TimeUnit',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field',
    'required': true
  }, {
    'name': 'interval',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'units',
    'type': 'enum',
    'values': TIME_UNITS,
    'array': true
  }, {
    'name': 'step',
    'type': 'number',
    'default': 1
  }, {
    'name': 'maxbins',
    'type': 'number',
    'default': 40
  }, {
    'name': 'extent',
    'type': 'date',
    'array': true
  }, {
    'name': 'timezone',
    'type': 'enum',
    'default': 'local',
    'values': ['local', 'utc']
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': OUTPUT
  }]
};
inherits(TimeUnit, Transform, {
  transform(_, pulse) {
    const field = _.field,
      band = _.interval !== false,
      utc = _.timezone === 'utc',
      floor = this._floor(_, pulse),
      offset = (utc ? utcInterval : timeInterval)(floor.unit).offset,
      as = _.as || OUTPUT,
      u0 = as[0],
      u1 = as[1],
      step = floor.step;
    let min = floor.start || Infinity,
      max = floor.stop || -Infinity,
      flag = pulse.ADD;
    if (_.modified() || pulse.changed(pulse.REM) || pulse.modified(accessorFields(field))) {
      pulse = pulse.reflow(true);
      flag = pulse.SOURCE;
      min = Infinity;
      max = -Infinity;
    }
    pulse.visit(flag, t => {
      const v = field(t);
      let a, b;
      if (v == null) {
        t[u0] = null;
        if (band) t[u1] = null;
      } else {
        t[u0] = a = b = floor(v);
        if (band) t[u1] = b = offset(a, step);
        if (a < min) min = a;
        if (b > max) max = b;
      }
    });
    floor.start = min;
    floor.stop = max;
    return pulse.modifies(band ? as : u0);
  },
  _floor(_, pulse) {
    const utc = _.timezone === 'utc';

    // get parameters
    const {
      units,
      step
    } = _.units ? {
      units: _.units,
      step: _.step || 1
    } : bin$1({
      extent: _.extent || extent(pulse.materialize(pulse.SOURCE).source, _.field),
      maxbins: _.maxbins
    });

    // check / standardize time units
    const tunits = timeUnits(units),
      prev = this.value || {},
      floor = (utc ? utcFloor : timeFloor)(tunits, step);
    floor.unit = peek$1(tunits);
    floor.units = tunits;
    floor.step = step;
    floor.start = prev.start;
    floor.stop = prev.stop;
    return this.value = floor;
  }
});

/**
 * An index that maps from unique, string-coerced, field values to tuples.
 * Assumes that the field serves as a unique key with no duplicate values.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The field accessor to index.
 */
function TupleIndex(params) {
  Transform.call(this, fastmap(), params);
}
inherits(TupleIndex, Transform, {
  transform(_, pulse) {
    const df = pulse.dataflow,
      field = _.field,
      index = this.value,
      set = t => index.set(field(t), t);
    let mod = true;
    if (_.modified('field') || pulse.modified(field.fields)) {
      index.clear();
      pulse.visit(pulse.SOURCE, set);
    } else if (pulse.changed()) {
      pulse.visit(pulse.REM, t => index.delete(field(t)));
      pulse.visit(pulse.ADD, set);
    } else {
      mod = false;
    }
    this.modified(mod);
    if (index.empty > df.cleanThreshold) df.runAfter(index.clean);
    return pulse.fork();
  }
});

/**
 * Extracts an array of values. Assumes the source data has already been
 * reduced as needed (e.g., by an upstream Aggregate transform).
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The domain field to extract.
 * @param {function(*,*): number} [params.sort] - An optional
 *   comparator function for sorting the values. The comparator will be
 *   applied to backing tuples prior to value extraction.
 */
function Values$1(params) {
  Transform.call(this, null, params);
}
inherits(Values$1, Transform, {
  transform(_, pulse) {
    const run = !this.value || _.modified('field') || _.modified('sort') || pulse.changed() || _.sort && pulse.modified(_.sort.fields);
    if (run) {
      this.value = (_.sort ? pulse.source.slice().sort(stableCompare(_.sort)) : pulse.source).map(_.field);
    }
  }
});

function WindowOp(op, field, param, as) {
  const fn = WindowOps[op](field, param);
  return {
    init: fn.init || zero$2,
    update: function (w, t) {
      t[as] = fn.next(w);
    }
  };
}
const WindowOps = {
  row_number: function () {
    return {
      next: w => w.index + 1
    };
  },
  rank: function () {
    let rank;
    return {
      init: () => rank = 1,
      next: w => {
        const i = w.index,
          data = w.data;
        return i && w.compare(data[i - 1], data[i]) ? rank = i + 1 : rank;
      }
    };
  },
  dense_rank: function () {
    let drank;
    return {
      init: () => drank = 1,
      next: w => {
        const i = w.index,
          d = w.data;
        return i && w.compare(d[i - 1], d[i]) ? ++drank : drank;
      }
    };
  },
  percent_rank: function () {
    const rank = WindowOps.rank(),
      next = rank.next;
    return {
      init: rank.init,
      next: w => (next(w) - 1) / (w.data.length - 1)
    };
  },
  cume_dist: function () {
    let cume;
    return {
      init: () => cume = 0,
      next: w => {
        const d = w.data,
          c = w.compare;
        let i = w.index;
        if (cume < i) {
          while (i + 1 < d.length && !c(d[i], d[i + 1])) ++i;
          cume = i;
        }
        return (1 + cume) / d.length;
      }
    };
  },
  ntile: function (field, num) {
    num = +num;
    if (!(num > 0)) error$1('ntile num must be greater than zero.');
    const cume = WindowOps.cume_dist(),
      next = cume.next;
    return {
      init: cume.init,
      next: w => Math.ceil(num * next(w))
    };
  },
  lag: function (field, offset) {
    offset = +offset || 1;
    return {
      next: w => {
        const i = w.index - offset;
        return i >= 0 ? field(w.data[i]) : null;
      }
    };
  },
  lead: function (field, offset) {
    offset = +offset || 1;
    return {
      next: w => {
        const i = w.index + offset,
          d = w.data;
        return i < d.length ? field(d[i]) : null;
      }
    };
  },
  first_value: function (field) {
    return {
      next: w => field(w.data[w.i0])
    };
  },
  last_value: function (field) {
    return {
      next: w => field(w.data[w.i1 - 1])
    };
  },
  nth_value: function (field, nth) {
    nth = +nth;
    if (!(nth > 0)) error$1('nth_value nth must be greater than zero.');
    return {
      next: w => {
        const i = w.i0 + (nth - 1);
        return i < w.i1 ? field(w.data[i]) : null;
      }
    };
  },
  prev_value: function (field) {
    let prev;
    return {
      init: () => prev = null,
      next: w => {
        const v = field(w.data[w.index]);
        return v != null ? prev = v : prev;
      }
    };
  },
  next_value: function (field) {
    let v, i;
    return {
      init: () => (v = null, i = -1),
      next: w => {
        const d = w.data;
        return w.index <= i ? v : (i = find$1(field, d, w.index)) < 0 ? (i = d.length, v = null) : v = field(d[i]);
      }
    };
  }
};
function find$1(field, data, index) {
  for (let n = data.length; index < n; ++index) {
    const v = field(data[index]);
    if (v != null) return index;
  }
  return -1;
}
const ValidWindowOps = Object.keys(WindowOps);

function WindowState(_) {
  const ops = array$2(_.ops),
    fields = array$2(_.fields),
    params = array$2(_.params),
    aggregate_params = array$2(_.aggregate_params),
    as = array$2(_.as),
    outputs = this.outputs = [],
    windows = this.windows = [],
    inputs = {},
    map = {},
    counts = [],
    measures = [];
  let countOnly = true;
  function visitInputs(f) {
    array$2(accessorFields(f)).forEach(_ => inputs[_] = 1);
  }
  visitInputs(_.sort);
  ops.forEach((op, i) => {
    const field = fields[i],
      param = params[i],
      aggregate_param = aggregate_params[i] || null,
      mname = accessorName(field),
      name = measureName(op, mname, as[i]);
    visitInputs(field);
    outputs.push(name);

    // Window operation
    if (has$1(WindowOps, op)) {
      windows.push(WindowOp(op, field, param, name));
    }

    // Aggregate operation
    else {
      if (field == null && op !== 'count') {
        error$1('Null aggregate field specified.');
      }
      if (op === 'count') {
        counts.push(name);
        return;
      }
      countOnly = false;
      let m = map[mname];
      if (!m) {
        m = map[mname] = [];
        m.field = field;
        measures.push(m);
      }
      m.push(createMeasure(op, aggregate_param, name));
    }
  });
  if (counts.length || measures.length) {
    this.cell = cell(measures, counts, countOnly);
  }
  this.inputs = Object.keys(inputs);
}
const prototype = WindowState.prototype;
prototype.init = function () {
  this.windows.forEach(_ => _.init());
  if (this.cell) this.cell.init();
};
prototype.update = function (w, t) {
  const cell = this.cell,
    wind = this.windows,
    data = w.data,
    m = wind && wind.length;
  let j;
  if (cell) {
    for (j = w.p0; j < w.i0; ++j) cell.rem(data[j]);
    for (j = w.p1; j < w.i1; ++j) cell.add(data[j]);
    cell.set(t);
  }
  for (j = 0; j < m; ++j) wind[j].update(w, t);
};
function cell(measures, counts, countOnly) {
  measures = measures.map(m => compileMeasures(m, m.field));
  const cell = {
    num: 0,
    agg: null,
    store: false,
    count: counts
  };
  if (!countOnly) {
    var n = measures.length,
      a = cell.agg = Array(n),
      i = 0;
    for (; i < n; ++i) a[i] = new measures[i](cell);
  }
  if (cell.store) {
    var store = cell.data = new TupleStore();
  }
  cell.add = function (t) {
    cell.num += 1;
    if (countOnly) return;
    if (store) store.add(t);
    for (let i = 0; i < n; ++i) {
      a[i].add(a[i].get(t), t);
    }
  };
  cell.rem = function (t) {
    cell.num -= 1;
    if (countOnly) return;
    if (store) store.rem(t);
    for (let i = 0; i < n; ++i) {
      a[i].rem(a[i].get(t), t);
    }
  };
  cell.set = function (t) {
    let i, n;

    // consolidate stored values
    if (store) store.values();

    // update tuple properties
    for (i = 0, n = counts.length; i < n; ++i) t[counts[i]] = cell.num;
    if (!countOnly) for (i = 0, n = a.length; i < n; ++i) a[i].set(t);
  };
  cell.init = function () {
    cell.num = 0;
    if (store) store.reset();
    for (let i = 0; i < n; ++i) a[i].init();
  };
  return cell;
}

/**
 * Perform window calculations and write results to the input stream.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(*,*): number} [params.sort] - A comparator function for sorting tuples within a window.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors by which to partition tuples into separate windows.
 * @param {Array<string>} params.ops - An array of strings indicating window operations to perform.
 * @param {Array<function(object): *>} [params.fields] - An array of accessors
 *   for data fields to use as inputs to window operations.
 * @param {Array<*>} [params.params] - An array of parameter values for window operations.
 * @param {Array<number>} [params.aggregate_params] - An optional array of parameter values for aggregation operations.
 * @param {Array<string>} [params.as] - An array of output field names for window operations.
 * @param {Array<number>} [params.frame] - Window frame definition as two-element array.
 * @param {boolean} [params.ignorePeers=false] - If true, base window frame boundaries on row
 *   number alone, ignoring peers with identical sort values. If false (default),
 *   the window boundaries will be adjusted to include peer values.
 */
function Window(params) {
  Transform.call(this, {}, params);
  this._mlen = 0;
  this._mods = [];
}
Window.Definition = {
  'type': 'Window',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'sort',
    'type': 'compare'
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'ops',
    'type': 'enum',
    'array': true,
    'values': ValidWindowOps.concat(ValidAggregateOps)
  }, {
    'name': 'params',
    'type': 'number',
    'null': true,
    'array': true
  }, {
    'name': 'aggregate_params',
    'type': 'number',
    'null': true,
    'array': true
  }, {
    'name': 'fields',
    'type': 'field',
    'null': true,
    'array': true
  }, {
    'name': 'as',
    'type': 'string',
    'null': true,
    'array': true
  }, {
    'name': 'frame',
    'type': 'number',
    'null': true,
    'array': true,
    'length': 2,
    'default': [null, 0]
  }, {
    'name': 'ignorePeers',
    'type': 'boolean',
    'default': false
  }]
};
inherits(Window, Transform, {
  transform(_, pulse) {
    this.stamp = pulse.stamp;
    const mod = _.modified(),
      cmp = stableCompare(_.sort),
      key = groupkey(_.groupby),
      group = t => this.group(key(t));

    // initialize window state
    let state = this.state;
    if (!state || mod) {
      state = this.state = new WindowState(_);
    }

    // partition input tuples
    if (mod || pulse.modified(state.inputs)) {
      this.value = {};
      pulse.visit(pulse.SOURCE, t => group(t).add(t));
    } else {
      pulse.visit(pulse.REM, t => group(t).remove(t));
      pulse.visit(pulse.ADD, t => group(t).add(t));
    }

    // perform window calculations for each modified partition
    for (let i = 0, n = this._mlen; i < n; ++i) {
      processPartition(this._mods[i], state, cmp, _);
    }
    this._mlen = 0;
    this._mods = [];

    // TODO don't reflow everything?
    return pulse.reflow(mod).modifies(state.outputs);
  },
  group(key) {
    let group = this.value[key];
    if (!group) {
      group = this.value[key] = SortedList(tupleid);
      group.stamp = -1;
    }
    if (group.stamp < this.stamp) {
      group.stamp = this.stamp;
      this._mods[this._mlen++] = group;
    }
    return group;
  }
});
function processPartition(list, state, cmp, _) {
  const sort = _.sort,
    range = sort && !_.ignorePeers,
    frame = _.frame || [null, 0],
    data = list.data(cmp),
    // use cmp for stable sort
    n = data.length,
    b = range ? bisector(sort) : null,
    w = {
      i0: 0,
      i1: 0,
      p0: 0,
      p1: 0,
      index: 0,
      data: data,
      compare: sort || constant$1(-1)
    };
  state.init();
  for (let i = 0; i < n; ++i) {
    setWindow(w, frame, i, n);
    if (range) adjustRange(w, b);
    state.update(w, data[i]);
  }
}
function setWindow(w, f, i, n) {
  w.p0 = w.i0;
  w.p1 = w.i1;
  w.i0 = f[0] == null ? 0 : Math.max(0, i - Math.abs(f[0]));
  w.i1 = f[1] == null ? n : Math.min(n, i + Math.abs(f[1]) + 1);
  w.index = i;
}

// if frame type is 'range', adjust window for peer values
function adjustRange(w, bisect) {
  const r0 = w.i0,
    r1 = w.i1 - 1,
    c = w.compare,
    d = w.data,
    n = d.length - 1;
  if (r0 > 0 && !c(d[r0], d[r0 - 1])) w.i0 = bisect.left(d, d[r0]);
  if (r1 < n && !c(d[r1], d[r1 + 1])) w.i1 = bisect.right(d, d[r1]);
}

const tx = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  aggregate: Aggregate$1,
  bin: Bin,
  collect: Collect$1,
  compare: Compare$1,
  countpattern: CountPattern,
  cross: Cross,
  density: Density,
  dotbin: DotBin,
  expression: Expression$1,
  extent: Extent,
  facet: Facet$1,
  field: Field$1,
  filter: Filter,
  flatten: Flatten,
  fold: Fold,
  formula: Formula,
  generate: Generate,
  impute: Impute,
  joinaggregate: JoinAggregate,
  kde: KDE,
  key: Key$1,
  load: Load$1,
  lookup: Lookup,
  multiextent: MultiExtent$1,
  multivalues: MultiValues$1,
  params: Params$2,
  pivot: Pivot,
  prefacet: PreFacet$1,
  project: Project,
  proxy: Proxy$1,
  quantile: Quantile$1,
  relay: Relay$1,
  sample: Sample,
  sequence: Sequence,
  sieve: Sieve$1,
  subflow: Subflow,
  timeunit: TimeUnit,
  tupleindex: TupleIndex,
  values: Values$1,
  window: Window
}, Symbol.toStringTag, { value: 'Module' }));

function domCanvas(w, h) {
  if (typeof document !== 'undefined' && document.createElement) {
    const c = document.createElement('canvas');
    if (c && c.getContext) {
      c.width = w;
      c.height = h;
      return c;
    }
  }
  return null;
}
const domImage = () => typeof Image !== 'undefined' ? Image : null;

function bandSpace (count, paddingInner, paddingOuter) {
  const space = count - paddingInner + paddingOuter * 2;
  return count ? space > 0 ? space : 1 : 0;
}

const Identity = 'identity';
const Linear = 'linear';
const Log = 'log';
const Pow = 'pow';
const Sqrt = 'sqrt';
const Symlog = 'symlog';
const Time = 'time';
const UTC = 'utc';
const Sequential = 'sequential';
const Diverging = 'diverging';
const Quantile = 'quantile';
const Quantize = 'quantize';
const Threshold = 'threshold';
const Ordinal = 'ordinal';
const Point = 'point';
const Band = 'band';
const BinOrdinal = 'bin-ordinal';

// categories
const Continuous = 'continuous';
const Discrete$1 = 'discrete';
const Discretizing = 'discretizing';
const Interpolating = 'interpolating';
const Temporal = 'temporal';

function invertRange (scale) {
  return function (_) {
    let lo = _[0],
      hi = _[1],
      t;
    if (hi < lo) {
      t = lo;
      lo = hi;
      hi = t;
    }
    return [scale.invert(lo), scale.invert(hi)];
  };
}

function invertRangeExtent (scale) {
  return function (_) {
    const range = scale.range();
    let lo = _[0],
      hi = _[1],
      min = -1,
      max,
      t,
      i,
      n;
    if (hi < lo) {
      t = lo;
      lo = hi;
      hi = t;
    }
    for (i = 0, n = range.length; i < n; ++i) {
      if (range[i] >= lo && range[i] <= hi) {
        if (min < 0) min = i;
        max = i;
      }
    }
    if (min < 0) return undefined;
    lo = scale.invertExtent(range[min]);
    hi = scale.invertExtent(range[max]);
    return [lo[0] === undefined ? lo[1] : lo[0], hi[1] === undefined ? hi[0] : hi[1]];
  };
}

function band() {
  const scale = ordinal().unknown(undefined),
    domain = scale.domain,
    ordinalRange = scale.range;
  let range$1 = [0, 1],
    step,
    bandwidth,
    round = false,
    paddingInner = 0,
    paddingOuter = 0,
    align = 0.5;
  delete scale.unknown;
  function rescale() {
    const n = domain().length,
      reverse = range$1[1] < range$1[0],
      stop = range$1[1 - reverse],
      space = bandSpace(n, paddingInner, paddingOuter);
    let start = range$1[reverse - 0];
    step = (stop - start) / (space || 1);
    if (round) {
      step = Math.floor(step);
    }
    start += (stop - start - step * (n - paddingInner)) * align;
    bandwidth = step * (1 - paddingInner);
    if (round) {
      start = Math.round(start);
      bandwidth = Math.round(bandwidth);
    }
    const values = range$4(n).map(i => start + step * i);
    return ordinalRange(reverse ? values.reverse() : values);
  }
  scale.domain = function (_) {
    if (arguments.length) {
      domain(_);
      return rescale();
    } else {
      return domain();
    }
  };
  scale.range = function (_) {
    if (arguments.length) {
      range$1 = [+_[0], +_[1]];
      return rescale();
    } else {
      return range$1.slice();
    }
  };
  scale.rangeRound = function (_) {
    range$1 = [+_[0], +_[1]];
    round = true;
    return rescale();
  };
  scale.bandwidth = function () {
    return bandwidth;
  };
  scale.step = function () {
    return step;
  };
  scale.round = function (_) {
    if (arguments.length) {
      round = !!_;
      return rescale();
    } else {
      return round;
    }
  };
  scale.padding = function (_) {
    if (arguments.length) {
      paddingOuter = Math.max(0, Math.min(1, _));
      paddingInner = paddingOuter;
      return rescale();
    } else {
      return paddingInner;
    }
  };
  scale.paddingInner = function (_) {
    if (arguments.length) {
      paddingInner = Math.max(0, Math.min(1, _));
      return rescale();
    } else {
      return paddingInner;
    }
  };
  scale.paddingOuter = function (_) {
    if (arguments.length) {
      paddingOuter = Math.max(0, Math.min(1, _));
      return rescale();
    } else {
      return paddingOuter;
    }
  };
  scale.align = function (_) {
    if (arguments.length) {
      align = Math.max(0, Math.min(1, _));
      return rescale();
    } else {
      return align;
    }
  };
  scale.invertRange = function (_) {
    // bail if range has null or undefined values
    if (_[0] == null || _[1] == null) return;
    const reverse = range$1[1] < range$1[0],
      values = reverse ? ordinalRange().reverse() : ordinalRange(),
      n = values.length - 1;
    let lo = +_[0],
      hi = +_[1],
      a,
      b,
      t;

    // bail if either range endpoint is invalid
    if (lo !== lo || hi !== hi) return;

    // order range inputs, bail if outside of scale range
    if (hi < lo) {
      t = lo;
      lo = hi;
      hi = t;
    }
    if (hi < values[0] || lo > range$1[1 - reverse]) return;

    // binary search to index into scale range
    a = Math.max(0, bisectRight$1(values, lo) - 1);
    b = lo === hi ? a : bisectRight$1(values, hi) - 1;

    // increment index a if lo is within padding gap
    if (lo - values[a] > bandwidth + 1e-10) ++a;
    if (reverse) {
      // map + swap
      t = a;
      a = n - b;
      b = n - t;
    }
    return a > b ? undefined : domain().slice(a, b + 1);
  };
  scale.invert = function (_) {
    const value = scale.invertRange([_, _]);
    return value ? value[0] : value;
  };
  scale.copy = function () {
    return band().domain(domain()).range(range$1).round(round).paddingInner(paddingInner).paddingOuter(paddingOuter).align(align);
  };
  return rescale();
}
function pointish(scale) {
  const copy = scale.copy;
  scale.padding = scale.paddingOuter;
  delete scale.paddingInner;
  scale.copy = function () {
    return pointish(copy());
  };
  return scale;
}
function point$3() {
  return pointish(band().paddingInner(1));
}

var map = Array.prototype.map;
function numbers(_) {
  return map.call(_, toNumber);
}

const slice$2 = Array.prototype.slice;

function scaleBinOrdinal() {
  let domain = [],
    range = [];
  function scale(x) {
    return x == null || x !== x ? undefined : range[(bisectRight$1(domain, x) - 1) % range.length];
  }
  scale.domain = function (_) {
    if (arguments.length) {
      domain = numbers(_);
      return scale;
    } else {
      return domain.slice();
    }
  };
  scale.range = function (_) {
    if (arguments.length) {
      range = slice$2.call(_);
      return scale;
    } else {
      return range.slice();
    }
  };
  scale.tickFormat = function (count, specifier) {
    return tickFormat$1(domain[0], peek$1(domain), count == null ? 10 : count, specifier);
  };
  scale.copy = function () {
    return scaleBinOrdinal().domain(scale.domain()).range(scale.range());
  };
  return scale;
}

/** Private scale registry: should not be exported */
const scales = new Map();
const VEGA_SCALE = Symbol('vega_scale');
function registerScale(scale) {
  scale[VEGA_SCALE] = true;
  return scale;
}

/**
 * Return true if object was created by a constructor from the vega-scale `scale` function.
 */
function isRegisteredScale(scale) {
  return scale && scale[VEGA_SCALE] === true;
}

/**
 * Augment scales with their type and needed inverse methods.
 */
function create$2(type, constructor, metadata) {
  const ctr = function scale() {
    const s = constructor();
    if (!s.invertRange) {
      s.invertRange = s.invert ? invertRange(s) : s.invertExtent ? invertRangeExtent(s) : undefined;
    }
    s.type = type;
    return registerScale(s);
  };
  ctr.metadata = toSet(array$2(metadata));
  return ctr;
}

/**
 * Registry function for adding and accessing scale constructor functions.
 * The *type* argument is a String indicating the name of the scale type.
 *
 * If the *scale* argument is not specified, this method returns the matching scale constructor in the registry, or `null` if not found.
 * If the *scale* argument is provided, it must be a scale constructor function to add to the registry under the given *type* name.
 * The *metadata* argument provides additional information to guide appropriate use of scales within Vega.
 *
 *  *metadata* can be either a string or string array. The valid string values are:
 * - `"continuous"` - the scale is defined over a continuous-valued domain.
 * - `"discrete"` - the scale is defined over a discrete domain and range.
 * - `"discretizing"` - the scale discretizes a continuous domain to a discrete range.
 * - `"interpolating"` - the scale range is defined using a color interpolator.
 * - `"log"` - the scale performs a logarithmic transform of the continuous domain.
 * - `"temporal"` - the scale domain is defined over date-time values.
 */
function scale$4(type, scale, metadata) {
  if (arguments.length > 1) {
    scales.set(type, create$2(type, scale, metadata));
    return this;
  } else {
    return isValidScaleType(type) ? scales.get(type) : undefined;
  }
}

// identity scale
scale$4(Identity, identity$1);

// continuous scales
scale$4(Linear, linear$1, Continuous);
scale$4(Log, log$3, [Continuous, Log]);
scale$4(Pow, pow$2, Continuous);
scale$4(Sqrt, sqrt, Continuous);
scale$4(Symlog, symlog$1, Continuous);
scale$4(Time, time$1, [Continuous, Temporal]);
scale$4(UTC, utcTime, [Continuous, Temporal]);

// sequential scales
scale$4(Sequential, sequential, [Continuous, Interpolating]); // backwards compat
scale$4(`${Sequential}-${Linear}`, sequential, [Continuous, Interpolating]);
scale$4(`${Sequential}-${Log}`, sequentialLog, [Continuous, Interpolating, Log]);
scale$4(`${Sequential}-${Pow}`, sequentialPow, [Continuous, Interpolating]);
scale$4(`${Sequential}-${Sqrt}`, sequentialSqrt, [Continuous, Interpolating]);
scale$4(`${Sequential}-${Symlog}`, sequentialSymlog, [Continuous, Interpolating]);

// diverging scales
scale$4(`${Diverging}-${Linear}`, diverging, [Continuous, Interpolating]);
scale$4(`${Diverging}-${Log}`, divergingLog, [Continuous, Interpolating, Log]);
scale$4(`${Diverging}-${Pow}`, divergingPow, [Continuous, Interpolating]);
scale$4(`${Diverging}-${Sqrt}`, divergingSqrt, [Continuous, Interpolating]);
scale$4(`${Diverging}-${Symlog}`, divergingSymlog, [Continuous, Interpolating]);

// discretizing scales
scale$4(Quantile, quantile$1, [Discretizing, Quantile]);
scale$4(Quantize, quantize$1, Discretizing);
scale$4(Threshold, threshold, Discretizing);

// discrete scales
scale$4(BinOrdinal, scaleBinOrdinal, [Discrete$1, Discretizing]);
scale$4(Ordinal, ordinal, Discrete$1);
scale$4(Band, band, Discrete$1);
scale$4(Point, point$3, Discrete$1);
function isValidScaleType(type) {
  return scales.has(type);
}
function hasType(key, type) {
  const s = scales.get(key);
  return s && s.metadata[type];
}
function isContinuous$1(key) {
  return hasType(key, Continuous);
}
function isDiscrete$2(key) {
  return hasType(key, Discrete$1);
}
function isDiscretizing$1(key) {
  return hasType(key, Discretizing);
}
function isLogarithmic(key) {
  return hasType(key, Log);
}
function isTemporal(key) {
  return hasType(key, Temporal);
}
function isInterpolating(key) {
  return hasType(key, Interpolating);
}
function isQuantile$1(key) {
  return hasType(key, Quantile);
}

const scaleProps = ['clamp', 'base', 'constant', 'exponent'];
function interpolateRange$1(interpolator, range) {
  const start = range[0],
    span = peek$1(range) - start;
  return function (i) {
    return interpolator(start + i * span);
  };
}
function interpolateColors(colors, type, gamma) {
  return piecewise(interpolate$1(type || 'rgb', gamma), colors);
}
function quantizeInterpolator(interpolator, count) {
  const samples = new Array(count),
    n = count + 1;
  for (let i = 0; i < count;) samples[i] = interpolator(++i / n);
  return samples;
}
function scaleFraction(scale$1, min, max) {
  const delta = max - min;
  let i, t, s;
  if (!delta || !Number.isFinite(delta)) {
    return constant$1(0.5);
  } else {
    i = (t = scale$1.type).indexOf('-');
    t = i < 0 ? t : t.slice(i + 1);
    s = scale$4(t)().domain([min, max]).range([0, 1]);
    scaleProps.forEach(m => scale$1[m] ? s[m](scale$1[m]()) : 0);
    return s;
  }
}
function interpolate$1(type, gamma) {
  const interp = $$1[method(type)];
  return gamma != null && interp && interp.gamma ? interp.gamma(gamma) : interp;
}
function method(type) {
  return 'interpolate' + type.toLowerCase().split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('');
}

const continuous = {
  blues: 'cfe1f2bed8eca8cee58fc1de74b2d75ba3cf4592c63181bd206fb2125ca40a4a90',
  greens: 'd3eecdc0e6baabdda594d3917bc77d60ba6c46ab5e329a512089430e7735036429',
  greys: 'e2e2e2d4d4d4c4c4c4b1b1b19d9d9d8888887575756262624d4d4d3535351e1e1e',
  oranges: 'fdd8b3fdc998fdb87bfda55efc9244f87f2cf06b18e4580bd14904b93d029f3303',
  purples: 'e2e1efd4d4e8c4c5e0b4b3d6a3a0cc928ec3827cb97566ae684ea25c3696501f8c',
  reds: 'fdc9b4fcb49afc9e80fc8767fa7051f6573fec3f2fdc2a25c81b1db21218970b13',
  blueGreen: 'd5efedc1e8e0a7ddd18bd2be70c6a958ba9144ad77319c5d2089460e7736036429',
  bluePurple: 'ccddecbad0e4a8c2dd9ab0d4919cc98d85be8b6db28a55a6873c99822287730f71',
  greenBlue: 'd3eecec5e8c3b1e1bb9bd8bb82cec269c2ca51b2cd3c9fc7288abd1675b10b60a1',
  orangeRed: 'fddcaffdcf9bfdc18afdad77fb9562f67d53ee6545e24932d32d1ebf130da70403',
  purpleBlue: 'dbdaebc8cee4b1c3de97b7d87bacd15b9fc93a90c01e7fb70b70ab056199045281',
  purpleBlueGreen: 'dbd8eac8cee4b0c3de93b7d872acd1549fc83892bb1c88a3097f8702736b016353',
  purpleRed: 'dcc9e2d3b3d7ce9eccd186c0da6bb2e14da0e23189d91e6fc61159ab07498f023a',
  redPurple: 'fccfccfcbec0faa9b8f98faff571a5ec539ddb3695c41b8aa908808d0179700174',
  yellowGreen: 'e4f4acd1eca0b9e2949ed68880c97c62bb6e47aa5e3297502083440e723b036034',
  yellowOrangeBrown: 'feeaa1fedd84fecc63feb746fca031f68921eb7215db5e0bc54c05ab3d038f3204',
  yellowOrangeRed: 'fee087fed16ffebd59fea849fd903efc7335f9522bee3423de1b20ca0b22af0225',
  blueOrange: '134b852f78b35da2cb9dcae1d2e5eff2f0ebfce0bafbbf74e8932fc5690d994a07',
  brownBlueGreen: '704108a0651ac79548e3c78af3e6c6eef1eac9e9e48ed1c74da79e187a72025147',
  purpleGreen: '5b1667834792a67fb6c9aed3e6d6e8eff0efd9efd5aedda971bb75368e490e5e29',
  purpleOrange: '4114696647968f83b7b9b4d6dadbebf3eeeafce0bafbbf74e8932fc5690d994a07',
  redBlue: '8c0d25bf363adf745ef4ae91fbdbc9f2efeed2e5ef9dcae15da2cb2f78b3134b85',
  redGrey: '8c0d25bf363adf745ef4ae91fcdccbfaf4f1e2e2e2c0c0c0969696646464343434',
  yellowGreenBlue: 'eff9bddbf1b4bde5b594d5b969c5be45b4c22c9ec02182b82163aa23479c1c3185',
  redYellowBlue: 'a50026d4322cf16e43fcac64fedd90faf8c1dcf1ecabd6e875abd04a74b4313695',
  redYellowGreen: 'a50026d4322cf16e43fcac63fedd8df9f7aed7ee8ea4d86e64bc6122964f006837',
  pinkYellowGreen: '8e0152c0267edd72adf0b3d6faddedf5f3efe1f2cab6de8780bb474f9125276419',
  spectral: '9e0142d13c4bf0704afcac63fedd8dfbf8b0e0f3a1a9dda269bda94288b55e4fa2',
  viridis: '440154470e61481a6c482575472f7d443a834144873d4e8a39568c35608d31688e2d708e2a788e27818e23888e21918d1f988b1fa08822a8842ab07f35b77943bf7154c56866cc5d7ad1518fd744a5db36bcdf27d2e21be9e51afde725',
  magma: '0000040404130b0924150e3720114b2c11603b0f704a107957157e651a80721f817f24828c29819a2e80a8327db6377ac43c75d1426fde4968e95462f1605df76f5cfa7f5efc8f65fe9f6dfeaf78febf84fece91fddea0fcedaffcfdbf',
  inferno: '0000040403130c0826170c3b240c4f330a5f420a68500d6c5d126e6b176e781c6d86216b932667a12b62ae305cbb3755c73e4cd24644dd513ae65c30ed6925f3771af8850ffb9506fca50afcb519fac62df6d645f2e661f3f484fcffa4',
  plasma: '0d088723069033059742039d5002a25d01a66a00a87801a88405a7900da49c179ea72198b12a90ba3488c33d80cb4779d35171da5a69e16462e76e5bed7953f2834cf68f44fa9a3dfca636fdb32ffec029fcce25f9dc24f5ea27f0f921',
  cividis: '00205100235800265d002961012b65042e670831690d346b11366c16396d1c3c6e213f6e26426e2c456e31476e374a6e3c4d6e42506e47536d4c566d51586e555b6e5a5e6e5e616e62646f66676f6a6a706e6d717270717573727976737c79747f7c75827f758682768985778c8877908b78938e789691789a94789e9778a19b78a59e77a9a177aea575b2a874b6ab73bbaf71c0b26fc5b66dc9b96acebd68d3c065d8c462ddc85fe2cb5ce7cf58ebd355f0d652f3da4ff7de4cfae249fce647',
  rainbow: '6e40aa883eb1a43db3bf3cafd83fa4ee4395fe4b83ff576eff6659ff7847ff8c38f3a130e2b72fcfcc36bee044aff05b8ff4576ff65b52f6673af27828ea8d1ddfa319d0b81cbecb23abd82f96e03d82e14c6edb5a5dd0664dbf6e40aa',
  sinebow: 'ff4040fc582af47218e78d0bd5a703bfbf00a7d5038de70b72f41858fc2a40ff402afc5818f4720be78d03d5a700bfbf03a7d50b8de71872f42a58fc4040ff582afc7218f48d0be7a703d5bf00bfd503a7e70b8df41872fc2a58ff4040',
  turbo: '23171b32204a3e2a71453493493eae4b49c54a53d7485ee44569ee4074f53c7ff8378af93295f72e9ff42ba9ef28b3e926bce125c5d925cdcf27d5c629dcbc2de3b232e9a738ee9d3ff39347f68950f9805afc7765fd6e70fe667cfd5e88fc5795fb51a1f84badf545b9f140c5ec3cd0e637dae034e4d931ecd12ef4c92bfac029ffb626ffad24ffa223ff9821ff8d1fff821dff771cfd6c1af76118f05616e84b14df4111d5380fcb2f0dc0260ab61f07ac1805a313029b0f00950c00910b00',
  browns: 'eedbbdecca96e9b97ae4a865dc9856d18954c7784cc0673fb85536ad44339f3632',
  tealBlues: 'bce4d89dd3d181c3cb65b3c245a2b9368fae347da0306a932c5985',
  teals: 'bbdfdfa2d4d58ac9c975bcbb61b0af4da5a43799982b8b8c1e7f7f127273006667',
  warmGreys: 'dcd4d0cec5c1c0b8b4b3aaa7a59c9998908c8b827f7e7673726866665c5a59504e',
  goldGreen: 'f4d166d5ca60b6c35c98bb597cb25760a6564b9c533f8f4f33834a257740146c36',
  goldOrange: 'f4d166f8be5cf8aa4cf5983bf3852aef701be2621fd65322c54923b142239e3a26',
  goldRed: 'f4d166f6be59f9aa51fc964ef6834bee734ae56249db5247cf4244c43141b71d3e',
  lightGreyRed: 'efe9e6e1dad7d5cbc8c8bdb9bbaea9cd967ddc7b43e15f19df4011dc000b',
  lightGreyTeal: 'e4eaead6dcddc8ced2b7c2c7a6b4bc64b0bf22a6c32295c11f85be1876bc',
  lightMulti: 'e0f1f2c4e9d0b0de9fd0e181f6e072f6c053f3993ef77440ef4a3c',
  lightOrange: 'f2e7daf7d5baf9c499fab184fa9c73f68967ef7860e8645bde515bd43d5b',
  lightTealBlue: 'e3e9e0c0dccf9aceca7abfc859afc0389fb9328dad2f7ca0276b95255988',
  darkBlue: '3232322d46681a5c930074af008cbf05a7ce25c0dd38daed50f3faffffff',
  darkGold: '3c3c3c584b37725e348c7631ae8b2bcfa424ecc31ef9de30fff184ffffff',
  darkGreen: '3a3a3a215748006f4d048942489e4276b340a6c63dd2d836ffeb2cffffaa',
  darkMulti: '3737371f5287197d8c29a86995ce3fffe800ffffff',
  darkRed: '3434347036339e3c38cc4037e75d1eec8620eeab29f0ce32ffeb2c'
};
const discrete = {
  accent: schemeAccent,
  category10: schemeCategory10,
  category20: '1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5',
  category20b: '393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6',
  category20c: '3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9',
  dark2: schemeDark2,
  observable10: schemeObservable10,
  paired: schemePaired,
  pastel1: schemePastel1,
  pastel2: schemePastel2,
  set1: schemeSet1,
  set2: schemeSet2,
  set3: schemeSet3,
  tableau10: '4c78a8f58518e4575672b7b254a24beeca3bb279a2ff9da69d755dbab0ac',
  tableau20: '4c78a89ecae9f58518ffbf7954a24b88d27ab79a20f2cf5b43989483bcb6e45756ff9d9879706ebab0acd67195fcbfd2b279a2d6a5c99e765fd8b5a5'
};

function colors(palette) {
  if (isArray(palette)) return palette;
  const n = palette.length / 6 | 0,
    c = new Array(n);
  for (let i = 0; i < n;) {
    c[i] = '#' + palette.slice(i * 6, ++i * 6);
  }
  return c;
}
function apply$1(_, f) {
  for (const k in _) scheme$1(k, f(_[k]));
}
const schemes = {};
apply$1(discrete, colors);
apply$1(continuous, _ => interpolateColors(colors(_)));
function scheme$1(name, scheme) {
  name = name && name.toLowerCase();
  if (arguments.length > 1) {
    schemes[name] = scheme;
    return this;
  } else {
    return schemes[name];
  }
}

const SymbolLegend = 'symbol';
const DiscreteLegend = 'discrete';
const GradientLegend = 'gradient';

const defaultFormatter = value => isArray(value) ? value.map(v => String(v)) : String(value);
const ascending = (a, b) => a[1] - b[1];
const descending = (a, b) => b[1] - a[1];

/**
 * Determine the tick count or interval function.
 * @param {Scale} scale - The scale for which to generate tick values.
 * @param {*} count - The desired tick count or interval specifier.
 * @param {number} minStep - The desired minimum step between tick values.
 * @return {*} - The tick count or interval function.
 */
function tickCount(scale, count, minStep) {
  let step;
  if (isNumber$1(count)) {
    if (scale.bins) {
      count = Math.max(count, scale.bins.length);
    }
    if (minStep != null) {
      count = Math.min(count, Math.floor(span(scale.domain()) / minStep || 1) + 1);
    }
  }
  if (isObject(count)) {
    step = count.step;
    count = count.interval;
  }
  if (isString(count)) {
    count = scale.type === Time ? timeInterval(count) : scale.type == UTC ? utcInterval(count) : error$1('Only time and utc scales accept interval strings.');
    if (step) count = count.every(step);
  }
  return count;
}

/**
 * Filter a set of candidate tick values, ensuring that only tick values
 * that lie within the scale range are included.
 * @param {Scale} scale - The scale for which to generate tick values.
 * @param {Array<*>} ticks - The candidate tick values.
 * @param {*} count - The tick count or interval function.
 * @return {Array<*>} - The filtered tick values.
 */
function validTicks(scale, ticks, count) {
  let range = scale.range(),
    lo = range[0],
    hi = peek$1(range),
    cmp = ascending;
  if (lo > hi) {
    range = hi;
    hi = lo;
    lo = range;
    cmp = descending;
  }
  lo = Math.floor(lo);
  hi = Math.ceil(hi);

  // filter ticks to valid values within the range
  // additionally sort ticks in range order (#2579)
  ticks = ticks.map(v => [v, scale(v)]).filter(_ => lo <= _[1] && _[1] <= hi).sort(cmp).map(_ => _[0]);
  if (count > 0 && ticks.length > 1) {
    const endpoints = [ticks[0], peek$1(ticks)];
    while (ticks.length > count && ticks.length >= 3) {
      ticks = ticks.filter((_, i) => !(i % 2));
    }
    if (ticks.length < 3) {
      ticks = endpoints;
    }
  }
  return ticks;
}

/**
 * Generate tick values for the given scale and approximate tick count or
 * interval value. If the scale has a 'ticks' method, it will be used to
 * generate the ticks, with the count argument passed as a parameter. If the
 * scale lacks a 'ticks' method, the full scale domain will be returned.
 * @param {Scale} scale - The scale for which to generate tick values.
 * @param {*} [count] - The approximate number of desired ticks.
 * @return {Array<*>} - The generated tick values.
 */
function tickValues(scale, count) {
  return scale.bins ? validTicks(scale, scale.bins, count) : scale.ticks ? scale.ticks(count) : scale.domain();
}

/**
 * Generate a label format function for a scale. If the scale has a
 * 'tickFormat' method, it will be used to generate the formatter, with the
 * count and specifier arguments passed as parameters. If the scale lacks a
 * 'tickFormat' method, the returned formatter performs simple string coercion.
 * If the input scale is a logarithmic scale and the format specifier does not
 * indicate a desired decimal precision, a special variable precision formatter
 * that automatically trims trailing zeroes will be generated.
 * @param {Scale} scale - The scale for which to generate the label formatter.
 * @param {*} [count] - The approximate number of desired ticks.
 * @param {string} [specifier] - The format specifier. Must be a legal d3
 *   specifier string (see https://github.com/d3/d3-format#formatSpecifier) or
 *   time multi-format specifier object.
 * @return {function(*):string} - The generated label formatter.
 */
function tickFormat(locale, scale, count, specifier, formatType, noSkip) {
  const type = scale.type;
  let format = defaultFormatter;
  if (type === Time || formatType === Time) {
    format = locale.timeFormat(specifier);
  } else if (type === UTC || formatType === UTC) {
    format = locale.utcFormat(specifier);
  } else if (isLogarithmic(type)) {
    const varfmt = locale.formatFloat(specifier);
    if (noSkip || scale.bins) {
      format = varfmt;
    } else {
      const test = tickLog(scale, count, false);
      format = _ => test(_) ? varfmt(_) : '';
    }
  } else if (scale.tickFormat) {
    // if d3 scale has tickFormat, it must be continuous
    const d = scale.domain();
    format = locale.formatSpan(d[0], d[d.length - 1], count, specifier);
  } else if (specifier) {
    format = locale.format(specifier);
  }
  return format;
}
function tickLog(scale, count, values) {
  const ticks = tickValues(scale, count),
    base = scale.base(),
    logb = Math.log(base),
    k = Math.max(1, base * count / ticks.length);

  // apply d3-scale's log format filter criteria
  const test = d => {
    let i = d / Math.pow(base, Math.round(Math.log(d) / logb));
    if (i * base < base - 0.5) i *= base;
    return i <= k;
  };
  return values ? ticks.filter(test) : test;
}

const symbols$2 = {
  [Quantile]: 'quantiles',
  [Quantize]: 'thresholds',
  [Threshold]: 'domain'
};
const formats = {
  [Quantile]: 'quantiles',
  [Quantize]: 'domain'
};
function labelValues(scale, count) {
  return scale.bins ? binValues(scale.bins) : scale.type === Log ? tickLog(scale, count, true) : symbols$2[scale.type] ? thresholdValues(scale[symbols$2[scale.type]]()) : tickValues(scale, count);
}
function thresholdFormat(locale, scale, specifier) {
  const _ = scale[formats[scale.type]](),
    n = _.length;
  let d = n > 1 ? _[1] - _[0] : _[0],
    i;
  for (i = 1; i < n; ++i) {
    d = Math.min(d, _[i] - _[i - 1]);
  }

  // tickCount = 3 ticks times 10 for increased resolution
  return locale.formatSpan(0, d, 3 * 10, specifier);
}
function thresholdValues(thresholds) {
  const values = [-Infinity].concat(thresholds);
  values.max = +Infinity;
  return values;
}
function binValues(bins) {
  const values = bins.slice(0, -1);
  values.max = peek$1(bins);
  return values;
}
const isDiscreteRange = scale => symbols$2[scale.type] || scale.bins;
function labelFormat(locale, scale, count, type, specifier, formatType, noSkip) {
  const format = formats[scale.type] && formatType !== Time && formatType !== UTC ? thresholdFormat(locale, scale, specifier) : tickFormat(locale, scale, count, specifier, formatType, noSkip);
  return type === SymbolLegend && isDiscreteRange(scale) ? formatRange(format) : type === DiscreteLegend ? formatDiscrete(format) : formatPoint(format);
}
const formatRange = format => (value, index, array) => {
  const limit = get$2(array[index + 1], get$2(array.max, +Infinity)),
    lo = formatValue$2(value, format),
    hi = formatValue$2(limit, format);
  return lo && hi ? lo + ' \u2013 ' + hi : hi ? '< ' + hi : '\u2265 ' + lo;
};
const get$2 = (value, dflt) => value != null ? value : dflt;
const formatDiscrete = format => (value, index) => index ? format(value) : null;
const formatPoint = format => value => format(value);
const formatValue$2 = (value, format) => Number.isFinite(value) ? format(value) : null;
function labelFraction(scale) {
  const domain = scale.domain(),
    count = domain.length - 1;
  let lo = +domain[0],
    hi = +peek$1(domain),
    span = hi - lo;
  if (scale.type === Threshold) {
    const adjust = count ? span / count : 0.1;
    lo -= adjust;
    hi += adjust;
    span = hi - lo;
  }
  return value => (value - lo) / span;
}

function format$1(locale, scale, specifier, formatType) {
  const type = formatType || scale.type;

  // replace abbreviated time specifiers to improve screen reader experience
  if (isString(specifier) && isTemporal(type)) {
    specifier = specifier.replace(/%a/g, '%A').replace(/%b/g, '%B');
  }
  return !specifier && type === Time ? locale.timeFormat('%A, %d %B %Y, %X') : !specifier && type === UTC ? locale.utcFormat('%A, %d %B %Y, %X UTC') : labelFormat(locale, scale, 5, null, specifier, formatType, true);
}
function domainCaption(locale, scale, opt) {
  opt = opt || {};
  const max = Math.max(3, opt.maxlen || 7),
    fmt = format$1(locale, scale, opt.format, opt.formatType);

  // if scale breaks domain into bins, describe boundaries
  if (isDiscretizing$1(scale.type)) {
    const v = labelValues(scale).slice(1).map(fmt),
      n = v.length;
    return `${n} boundar${n === 1 ? 'y' : 'ies'}: ${v.join(', ')}`;
  }

  // if scale domain is discrete, list values
  else if (isDiscrete$2(scale.type)) {
    const d = scale.domain(),
      n = d.length,
      v = n > max ? d.slice(0, max - 2).map(fmt).join(', ') + ', ending with ' + d.slice(-1).map(fmt) : d.map(fmt).join(', ');
    return `${n} value${n === 1 ? '' : 's'}: ${v}`;
  }

  // if scale domain is continuous, describe value range
  else {
    const d = scale.domain();
    return `values from ${fmt(d[0])} to ${fmt(peek$1(d))}`;
  }
}

let gradient_id = 0;
function resetSVGGradientId() {
  gradient_id = 0;
}
const patternPrefix = 'p_';
function isGradient(value) {
  return value && value.gradient;
}
function gradientRef(g, defs, base) {
  const type = g.gradient;
  let id = g.id,
    prefix = type === 'radial' ? patternPrefix : '';

  // check id, assign default values as needed
  if (!id) {
    id = g.id = 'gradient_' + gradient_id++;
    if (type === 'radial') {
      g.x1 = get$1(g.x1, 0.5);
      g.y1 = get$1(g.y1, 0.5);
      g.r1 = get$1(g.r1, 0);
      g.x2 = get$1(g.x2, 0.5);
      g.y2 = get$1(g.y2, 0.5);
      g.r2 = get$1(g.r2, 0.5);
      prefix = patternPrefix;
    } else {
      g.x1 = get$1(g.x1, 0);
      g.y1 = get$1(g.y1, 0);
      g.x2 = get$1(g.x2, 1);
      g.y2 = get$1(g.y2, 0);
    }
  }

  // register definition
  defs[id] = g;

  // return url reference
  return 'url(' + (base || '') + '#' + prefix + id + ')';
}
function get$1(val, def) {
  return val != null ? val : def;
}
function Gradient$1 (p0, p1) {
  var stops = [],
    gradient;
  return gradient = {
    gradient: 'linear',
    x1: p0 ? p0[0] : 0,
    y1: p0 ? p0[1] : 0,
    x2: p1 ? p1[0] : 1,
    y2: p1 ? p1[1] : 0,
    stops: stops,
    stop: function (offset, color) {
      stops.push({
        offset: offset,
        color: color
      });
      return gradient;
    }
  };
}

const lookup$4 = {
  'basis': {
    curve: curveBasis
  },
  'basis-closed': {
    curve: curveBasisClosed
  },
  'basis-open': {
    curve: curveBasisOpen
  },
  'bundle': {
    curve: curveBundle,
    tension: 'beta',
    value: 0.85
  },
  'cardinal': {
    curve: curveCardinal,
    tension: 'tension',
    value: 0
  },
  'cardinal-open': {
    curve: curveCardinalOpen,
    tension: 'tension',
    value: 0
  },
  'cardinal-closed': {
    curve: curveCardinalClosed,
    tension: 'tension',
    value: 0
  },
  'catmull-rom': {
    curve: curveCatmullRom,
    tension: 'alpha',
    value: 0.5
  },
  'catmull-rom-closed': {
    curve: curveCatmullRomClosed,
    tension: 'alpha',
    value: 0.5
  },
  'catmull-rom-open': {
    curve: curveCatmullRomOpen,
    tension: 'alpha',
    value: 0.5
  },
  'linear': {
    curve: curveLinear
  },
  'linear-closed': {
    curve: curveLinearClosed
  },
  'monotone': {
    horizontal: monotoneY,
    vertical: monotoneX
  },
  'natural': {
    curve: curveNatural
  },
  'step': {
    curve: curveStep
  },
  'step-after': {
    curve: stepAfter
  },
  'step-before': {
    curve: stepBefore
  }
};
function curves(type, orientation, tension) {
  var entry = has$1(lookup$4, type) && lookup$4[type],
    curve = null;
  if (entry) {
    curve = entry.curve || entry[orientation || 'vertical'];
    if (entry.tension && tension != null) {
      curve = curve[entry.tension](tension);
    }
  }
  return curve;
}

const paramCounts = {
  m: 2,
  l: 2,
  h: 1,
  v: 1,
  z: 0,
  c: 6,
  s: 4,
  q: 4,
  t: 2,
  a: 7
};
const commandPattern = /[mlhvzcsqta]([^mlhvzcsqta]+|$)/gi;
const numberPattern = /^[+-]?(([0-9]*\.[0-9]+)|([0-9]+\.)|([0-9]+))([eE][+-]?[0-9]+)?/;
const spacePattern = /^((\s+,?\s*)|(,\s*))/;
const flagPattern = /^[01]/;
function parse$3(path) {
  const commands = [];
  const matches = path.match(commandPattern) || [];
  matches.forEach(str => {
    let cmd = str[0];
    const type = cmd.toLowerCase();

    // parse parameters
    const paramCount = paramCounts[type];
    const params = parseParams(type, paramCount, str.slice(1).trim());
    const count = params.length;

    // error checking based on parameter count
    if (count < paramCount || count && count % paramCount !== 0) {
      throw Error('Invalid SVG path, incorrect parameter count');
    }

    // register the command
    commands.push([cmd, ...params.slice(0, paramCount)]);

    // exit now if we're done, also handles zero-param 'z'
    if (count === paramCount) {
      return;
    }

    // handle implicit line-to
    if (type === 'm') {
      cmd = cmd === 'M' ? 'L' : 'l';
    }

    // repeat command when given extended param list
    for (let i = paramCount; i < count; i += paramCount) {
      commands.push([cmd, ...params.slice(i, i + paramCount)]);
    }
  });
  return commands;
}
function parseParams(type, paramCount, segment) {
  const params = [];
  for (let index = 0; paramCount && index < segment.length;) {
    for (let i = 0; i < paramCount; ++i) {
      const pattern = type === 'a' && (i === 3 || i === 4) ? flagPattern : numberPattern;
      const match = segment.slice(index).match(pattern);
      if (match === null) {
        throw Error('Invalid SVG path, incorrect parameter type');
      }
      index += match[0].length;
      params.push(+match[0]);
      const ws = segment.slice(index).match(spacePattern);
      if (ws !== null) {
        index += ws[0].length;
      }
    }
  }
  return params;
}

const DegToRad = Math.PI / 180;
const Epsilon = 1e-14;
const HalfPi = Math.PI / 2;
const Tau = Math.PI * 2;
const HalfSqrt3 = Math.sqrt(3) / 2;

var segmentCache = {};
var bezierCache = {};
var join$1 = [].join;

// Copied from Inkscape svgtopdf, thanks!
function segments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
  const key = join$1.call(arguments);
  if (segmentCache[key]) {
    return segmentCache[key];
  }
  const th = rotateX * DegToRad;
  const sin_th = Math.sin(th);
  const cos_th = Math.cos(th);
  rx = Math.abs(rx);
  ry = Math.abs(ry);
  const px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
  const py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
  let pl = px * px / (rx * rx) + py * py / (ry * ry);
  if (pl > 1) {
    pl = Math.sqrt(pl);
    rx *= pl;
    ry *= pl;
  }
  const a00 = cos_th / rx;
  const a01 = sin_th / rx;
  const a10 = -sin_th / ry;
  const a11 = cos_th / ry;
  const x0 = a00 * ox + a01 * oy;
  const y0 = a10 * ox + a11 * oy;
  const x1 = a00 * x + a01 * y;
  const y1 = a10 * x + a11 * y;
  const d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0);
  let sfactor_sq = 1 / d - 0.25;
  if (sfactor_sq < 0) sfactor_sq = 0;
  let sfactor = Math.sqrt(sfactor_sq);
  if (sweep == large) sfactor = -sfactor;
  const xc = 0.5 * (x0 + x1) - sfactor * (y1 - y0);
  const yc = 0.5 * (y0 + y1) + sfactor * (x1 - x0);
  const th0 = Math.atan2(y0 - yc, x0 - xc);
  const th1 = Math.atan2(y1 - yc, x1 - xc);
  let th_arc = th1 - th0;
  if (th_arc < 0 && sweep === 1) {
    th_arc += Tau;
  } else if (th_arc > 0 && sweep === 0) {
    th_arc -= Tau;
  }
  const segs = Math.ceil(Math.abs(th_arc / (HalfPi + 0.001)));
  const result = [];
  for (let i = 0; i < segs; ++i) {
    const th2 = th0 + i * th_arc / segs;
    const th3 = th0 + (i + 1) * th_arc / segs;
    result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th];
  }
  return segmentCache[key] = result;
}
function bezier(params) {
  const key = join$1.call(params);
  if (bezierCache[key]) {
    return bezierCache[key];
  }
  var cx = params[0],
    cy = params[1],
    th0 = params[2],
    th1 = params[3],
    rx = params[4],
    ry = params[5],
    sin_th = params[6],
    cos_th = params[7];
  const a00 = cos_th * rx;
  const a01 = -sin_th * ry;
  const a10 = sin_th * rx;
  const a11 = cos_th * ry;
  const cos_th0 = Math.cos(th0);
  const sin_th0 = Math.sin(th0);
  const cos_th1 = Math.cos(th1);
  const sin_th1 = Math.sin(th1);
  const th_half = 0.5 * (th1 - th0);
  const sin_th_h2 = Math.sin(th_half * 0.5);
  const t = 8 / 3 * sin_th_h2 * sin_th_h2 / Math.sin(th_half);
  const x1 = cx + cos_th0 - t * sin_th0;
  const y1 = cy + sin_th0 + t * cos_th0;
  const x3 = cx + cos_th1;
  const y3 = cy + sin_th1;
  const x2 = x3 + t * sin_th1;
  const y2 = y3 - t * cos_th1;
  return bezierCache[key] = [a00 * x1 + a01 * y1, a10 * x1 + a11 * y1, a00 * x2 + a01 * y2, a10 * x2 + a11 * y2, a00 * x3 + a01 * y3, a10 * x3 + a11 * y3];
}

const temp = ['l', 0, 0, 0, 0, 0, 0, 0];
function scale$1$1(current, sX, sY) {
  const c = temp[0] = current[0];
  if (c === 'a' || c === 'A') {
    temp[1] = sX * current[1];
    temp[2] = sY * current[2];
    temp[3] = current[3];
    temp[4] = current[4];
    temp[5] = current[5];
    temp[6] = sX * current[6];
    temp[7] = sY * current[7];
  } else if (c === 'h' || c === 'H') {
    temp[1] = sX * current[1];
  } else if (c === 'v' || c === 'V') {
    temp[1] = sY * current[1];
  } else {
    for (var i = 1, n = current.length; i < n; ++i) {
      temp[i] = (i % 2 == 1 ? sX : sY) * current[i];
    }
  }
  return temp;
}
function pathRender (context, path, l, t, sX, sY) {
  var current,
    // current instruction
    previous = null,
    x = 0,
    // current x
    y = 0,
    // current y
    controlX = 0,
    // current control point x
    controlY = 0,
    // current control point y
    tempX,
    tempY,
    tempControlX,
    tempControlY,
    anchorX = 0,
    anchorY = 0;
  if (l == null) l = 0;
  if (t == null) t = 0;
  if (sX == null) sX = 1;
  if (sY == null) sY = sX;
  if (context.beginPath) context.beginPath();
  for (var i = 0, len = path.length; i < len; ++i) {
    current = path[i];
    if (sX !== 1 || sY !== 1) {
      current = scale$1$1(current, sX, sY);
    }
    switch (current[0]) {
      // first letter

      case 'l':
        // lineto, relative
        x += current[1];
        y += current[2];
        context.lineTo(x + l, y + t);
        break;
      case 'L':
        // lineto, absolute
        x = current[1];
        y = current[2];
        context.lineTo(x + l, y + t);
        break;
      case 'h':
        // horizontal lineto, relative
        x += current[1];
        context.lineTo(x + l, y + t);
        break;
      case 'H':
        // horizontal lineto, absolute
        x = current[1];
        context.lineTo(x + l, y + t);
        break;
      case 'v':
        // vertical lineto, relative
        y += current[1];
        context.lineTo(x + l, y + t);
        break;
      case 'V':
        // verical lineto, absolute
        y = current[1];
        context.lineTo(x + l, y + t);
        break;
      case 'm':
        // moveTo, relative
        x += current[1];
        y += current[2];
        anchorX = x;
        anchorY = y;
        context.moveTo(x + l, y + t);
        break;
      case 'M':
        // moveTo, absolute
        x = current[1];
        y = current[2];
        anchorX = x;
        anchorY = y;
        context.moveTo(x + l, y + t);
        break;
      case 'c':
        // bezierCurveTo, relative
        tempX = x + current[5];
        tempY = y + current[6];
        controlX = x + current[3];
        controlY = y + current[4];
        context.bezierCurveTo(x + current[1] + l,
        // x1
        y + current[2] + t,
        // y1
        controlX + l,
        // x2
        controlY + t,
        // y2
        tempX + l, tempY + t);
        x = tempX;
        y = tempY;
        break;
      case 'C':
        // bezierCurveTo, absolute
        x = current[5];
        y = current[6];
        controlX = current[3];
        controlY = current[4];
        context.bezierCurveTo(current[1] + l, current[2] + t, controlX + l, controlY + t, x + l, y + t);
        break;
      case 's':
        // shorthand cubic bezierCurveTo, relative
        // transform to absolute x,y
        tempX = x + current[3];
        tempY = y + current[4];
        // calculate reflection of previous control points
        controlX = 2 * x - controlX;
        controlY = 2 * y - controlY;
        context.bezierCurveTo(controlX + l, controlY + t, x + current[1] + l, y + current[2] + t, tempX + l, tempY + t);

        // set control point to 2nd one of this command
        // the first control point is assumed to be the reflection of
        // the second control point on the previous command relative
        // to the current point.
        controlX = x + current[1];
        controlY = y + current[2];
        x = tempX;
        y = tempY;
        break;
      case 'S':
        // shorthand cubic bezierCurveTo, absolute
        tempX = current[3];
        tempY = current[4];
        // calculate reflection of previous control points
        controlX = 2 * x - controlX;
        controlY = 2 * y - controlY;
        context.bezierCurveTo(controlX + l, controlY + t, current[1] + l, current[2] + t, tempX + l, tempY + t);
        x = tempX;
        y = tempY;
        // set control point to 2nd one of this command
        // the first control point is assumed to be the reflection of
        // the second control point on the previous command relative
        // to the current point.
        controlX = current[1];
        controlY = current[2];
        break;
      case 'q':
        // quadraticCurveTo, relative
        // transform to absolute x,y
        tempX = x + current[3];
        tempY = y + current[4];
        controlX = x + current[1];
        controlY = y + current[2];
        context.quadraticCurveTo(controlX + l, controlY + t, tempX + l, tempY + t);
        x = tempX;
        y = tempY;
        break;
      case 'Q':
        // quadraticCurveTo, absolute
        tempX = current[3];
        tempY = current[4];
        context.quadraticCurveTo(current[1] + l, current[2] + t, tempX + l, tempY + t);
        x = tempX;
        y = tempY;
        controlX = current[1];
        controlY = current[2];
        break;
      case 't':
        // shorthand quadraticCurveTo, relative

        // transform to absolute x,y
        tempX = x + current[1];
        tempY = y + current[2];
        if (previous[0].match(/[QqTt]/) === null) {
          // If there is no previous command or if the previous command was not a Q, q, T or t,
          // assume the control point is coincident with the current point
          controlX = x;
          controlY = y;
        } else if (previous[0] === 't') {
          // calculate reflection of previous control points for t
          controlX = 2 * x - tempControlX;
          controlY = 2 * y - tempControlY;
        } else if (previous[0] === 'q') {
          // calculate reflection of previous control points for q
          controlX = 2 * x - controlX;
          controlY = 2 * y - controlY;
        }
        tempControlX = controlX;
        tempControlY = controlY;
        context.quadraticCurveTo(controlX + l, controlY + t, tempX + l, tempY + t);
        x = tempX;
        y = tempY;
        controlX = x + current[1];
        controlY = y + current[2];
        break;
      case 'T':
        tempX = current[1];
        tempY = current[2];

        // calculate reflection of previous control points
        controlX = 2 * x - controlX;
        controlY = 2 * y - controlY;
        context.quadraticCurveTo(controlX + l, controlY + t, tempX + l, tempY + t);
        x = tempX;
        y = tempY;
        break;
      case 'a':
        drawArc(context, x + l, y + t, [current[1], current[2], current[3], current[4], current[5], current[6] + x + l, current[7] + y + t]);
        x += current[6];
        y += current[7];
        break;
      case 'A':
        drawArc(context, x + l, y + t, [current[1], current[2], current[3], current[4], current[5], current[6] + l, current[7] + t]);
        x = current[6];
        y = current[7];
        break;
      case 'z':
      case 'Z':
        x = anchorX;
        y = anchorY;
        context.closePath();
        break;
    }
    previous = current;
  }
}
function drawArc(context, x, y, coords) {
  const seg = segments(coords[5],
  // end x
  coords[6],
  // end y
  coords[0],
  // radius x
  coords[1],
  // radius y
  coords[3],
  // large flag
  coords[4],
  // sweep flag
  coords[2],
  // rotation
  x, y);
  for (let i = 0; i < seg.length; ++i) {
    const bez = bezier(seg[i]);
    context.bezierCurveTo(bez[0], bez[1], bez[2], bez[3], bez[4], bez[5]);
  }
}

const Tan30 = 0.5773502691896257;
const builtins = {
  'circle': {
    draw: function (context, size) {
      const r = Math.sqrt(size) / 2;
      context.moveTo(r, 0);
      context.arc(0, 0, r, 0, Tau);
    }
  },
  'cross': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        s = r / 2.5;
      context.moveTo(-r, -s);
      context.lineTo(-r, s);
      context.lineTo(-s, s);
      context.lineTo(-s, r);
      context.lineTo(s, r);
      context.lineTo(s, s);
      context.lineTo(r, s);
      context.lineTo(r, -s);
      context.lineTo(s, -s);
      context.lineTo(s, -r);
      context.lineTo(-s, -r);
      context.lineTo(-s, -s);
      context.closePath();
    }
  },
  'diamond': {
    draw: function (context, size) {
      const r = Math.sqrt(size) / 2;
      context.moveTo(-r, 0);
      context.lineTo(0, -r);
      context.lineTo(r, 0);
      context.lineTo(0, r);
      context.closePath();
    }
  },
  'square': {
    draw: function (context, size) {
      var w = Math.sqrt(size),
        x = -w / 2;
      context.rect(x, x, w, w);
    }
  },
  'arrow': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        s = r / 7,
        t = r / 2.5,
        v = r / 8;
      context.moveTo(-s, r);
      context.lineTo(s, r);
      context.lineTo(s, -v);
      context.lineTo(t, -v);
      context.lineTo(0, -r);
      context.lineTo(-t, -v);
      context.lineTo(-s, -v);
      context.closePath();
    }
  },
  'wedge': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        h = HalfSqrt3 * r,
        o = h - r * Tan30,
        b = r / 4;
      context.moveTo(0, -h - o);
      context.lineTo(-b, h - o);
      context.lineTo(b, h - o);
      context.closePath();
    }
  },
  'triangle': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        h = HalfSqrt3 * r,
        o = h - r * Tan30;
      context.moveTo(0, -h - o);
      context.lineTo(-r, h - o);
      context.lineTo(r, h - o);
      context.closePath();
    }
  },
  'triangle-up': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        h = HalfSqrt3 * r;
      context.moveTo(0, -h);
      context.lineTo(-r, h);
      context.lineTo(r, h);
      context.closePath();
    }
  },
  'triangle-down': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        h = HalfSqrt3 * r;
      context.moveTo(0, h);
      context.lineTo(-r, -h);
      context.lineTo(r, -h);
      context.closePath();
    }
  },
  'triangle-right': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        h = HalfSqrt3 * r;
      context.moveTo(h, 0);
      context.lineTo(-h, -r);
      context.lineTo(-h, r);
      context.closePath();
    }
  },
  'triangle-left': {
    draw: function (context, size) {
      var r = Math.sqrt(size) / 2,
        h = HalfSqrt3 * r;
      context.moveTo(-h, 0);
      context.lineTo(h, -r);
      context.lineTo(h, r);
      context.closePath();
    }
  },
  'stroke': {
    draw: function (context, size) {
      const r = Math.sqrt(size) / 2;
      context.moveTo(-r, 0);
      context.lineTo(r, 0);
    }
  }
};
function symbols$1(_) {
  return has$1(builtins, _) ? builtins[_] : customSymbol(_);
}
var custom = {};
function customSymbol(path) {
  if (!has$1(custom, path)) {
    const parsed = parse$3(path);
    custom[path] = {
      draw: function (context, size) {
        pathRender(context, parsed, 0, 0, Math.sqrt(size) / 2);
      }
    };
  }
  return custom[path];
}

// See http://spencermortensen.com/articles/bezier-circle/
const C = 0.448084975506; // C = 1 - c

function rectangleX(d) {
  return d.x;
}
function rectangleY(d) {
  return d.y;
}
function rectangleWidth(d) {
  return d.width;
}
function rectangleHeight(d) {
  return d.height;
}
function number$3(_) {
  return typeof _ === 'function' ? _ : () => +_;
}
function clamp(value, min, max) {
  return Math.max(min, Math.min(value, max));
}
function vg_rect () {
  var x = rectangleX,
    y = rectangleY,
    width = rectangleWidth,
    height = rectangleHeight,
    crTL = number$3(0),
    crTR = crTL,
    crBL = crTL,
    crBR = crTL,
    context = null;
  function rectangle(_, x0, y0) {
    var buffer,
      x1 = x0 != null ? x0 : +x.call(this, _),
      y1 = y0 != null ? y0 : +y.call(this, _),
      w = +width.call(this, _),
      h = +height.call(this, _),
      s = Math.min(w, h) / 2,
      tl = clamp(+crTL.call(this, _), 0, s),
      tr = clamp(+crTR.call(this, _), 0, s),
      bl = clamp(+crBL.call(this, _), 0, s),
      br = clamp(+crBR.call(this, _), 0, s);
    if (!context) context = buffer = path$3();
    if (tl <= 0 && tr <= 0 && bl <= 0 && br <= 0) {
      context.rect(x1, y1, w, h);
    } else {
      var x2 = x1 + w,
        y2 = y1 + h;
      context.moveTo(x1 + tl, y1);
      context.lineTo(x2 - tr, y1);
      context.bezierCurveTo(x2 - C * tr, y1, x2, y1 + C * tr, x2, y1 + tr);
      context.lineTo(x2, y2 - br);
      context.bezierCurveTo(x2, y2 - C * br, x2 - C * br, y2, x2 - br, y2);
      context.lineTo(x1 + bl, y2);
      context.bezierCurveTo(x1 + C * bl, y2, x1, y2 - C * bl, x1, y2 - bl);
      context.lineTo(x1, y1 + tl);
      context.bezierCurveTo(x1, y1 + C * tl, x1 + C * tl, y1, x1 + tl, y1);
      context.closePath();
    }
    if (buffer) {
      context = null;
      return buffer + '' || null;
    }
  }
  rectangle.x = function (_) {
    if (arguments.length) {
      x = number$3(_);
      return rectangle;
    } else {
      return x;
    }
  };
  rectangle.y = function (_) {
    if (arguments.length) {
      y = number$3(_);
      return rectangle;
    } else {
      return y;
    }
  };
  rectangle.width = function (_) {
    if (arguments.length) {
      width = number$3(_);
      return rectangle;
    } else {
      return width;
    }
  };
  rectangle.height = function (_) {
    if (arguments.length) {
      height = number$3(_);
      return rectangle;
    } else {
      return height;
    }
  };
  rectangle.cornerRadius = function (tl, tr, br, bl) {
    if (arguments.length) {
      crTL = number$3(tl);
      crTR = tr != null ? number$3(tr) : crTL;
      crBR = br != null ? number$3(br) : crTL;
      crBL = bl != null ? number$3(bl) : crTR;
      return rectangle;
    } else {
      return crTL;
    }
  };
  rectangle.context = function (_) {
    if (arguments.length) {
      context = _ == null ? null : _;
      return rectangle;
    } else {
      return context;
    }
  };
  return rectangle;
}

function vg_trail () {
  var x,
    y,
    size,
    defined,
    context = null,
    ready,
    x1,
    y1,
    r1;
  function point(x2, y2, w2) {
    const r2 = w2 / 2;
    if (ready) {
      var ux = y1 - y2,
        uy = x2 - x1;
      if (ux || uy) {
        // get normal vector
        var ud = Math.hypot(ux, uy),
          rx = (ux /= ud) * r1,
          ry = (uy /= ud) * r1,
          t = Math.atan2(uy, ux);

        // draw segment
        context.moveTo(x1 - rx, y1 - ry);
        context.lineTo(x2 - ux * r2, y2 - uy * r2);
        context.arc(x2, y2, r2, t - Math.PI, t);
        context.lineTo(x1 + rx, y1 + ry);
        context.arc(x1, y1, r1, t, t + Math.PI);
      } else {
        context.arc(x2, y2, r2, 0, Tau);
      }
      context.closePath();
    } else {
      ready = 1;
    }
    x1 = x2;
    y1 = y2;
    r1 = r2;
  }
  function trail(data) {
    var i,
      n = data.length,
      d,
      defined0 = false,
      buffer;
    if (context == null) context = buffer = path$3();
    for (i = 0; i <= n; ++i) {
      if (!(i < n && defined(d = data[i], i, data)) === defined0) {
        if (defined0 = !defined0) ready = 0;
      }
      if (defined0) point(+x(d, i, data), +y(d, i, data), +size(d, i, data));
    }
    if (buffer) {
      context = null;
      return buffer + '' || null;
    }
  }
  trail.x = function (_) {
    if (arguments.length) {
      x = _;
      return trail;
    } else {
      return x;
    }
  };
  trail.y = function (_) {
    if (arguments.length) {
      y = _;
      return trail;
    } else {
      return y;
    }
  };
  trail.size = function (_) {
    if (arguments.length) {
      size = _;
      return trail;
    } else {
      return size;
    }
  };
  trail.defined = function (_) {
    if (arguments.length) {
      defined = _;
      return trail;
    } else {
      return defined;
    }
  };
  trail.context = function (_) {
    if (arguments.length) {
      if (_ == null) {
        context = null;
      } else {
        context = _;
      }
      return trail;
    } else {
      return context;
    }
  };
  return trail;
}

function value$1(a, b) {
  return a != null ? a : b;
}
const x = item => item.x || 0,
  y = item => item.y || 0,
  w$1 = item => item.width || 0,
  h = item => item.height || 0,
  xw = item => (item.x || 0) + (item.width || 0),
  yh = item => (item.y || 0) + (item.height || 0),
  sa = item => item.startAngle || 0,
  ea = item => item.endAngle || 0,
  pa = item => item.padAngle || 0,
  ir = item => item.innerRadius || 0,
  or = item => item.outerRadius || 0,
  cr = item => item.cornerRadius || 0,
  tl = item => value$1(item.cornerRadiusTopLeft, item.cornerRadius) || 0,
  tr = item => value$1(item.cornerRadiusTopRight, item.cornerRadius) || 0,
  br = item => value$1(item.cornerRadiusBottomRight, item.cornerRadius) || 0,
  bl = item => value$1(item.cornerRadiusBottomLeft, item.cornerRadius) || 0,
  sz = item => value$1(item.size, 64),
  ts = item => item.size || 1,
  def = item => !(item.defined === false),
  type$1 = item => symbols$1(item.shape || 'circle');
const arcShape = d3arc().startAngle(sa).endAngle(ea).padAngle(pa).innerRadius(ir).outerRadius(or).cornerRadius(cr),
  areavShape = area$2$1().x(x).y1(y).y0(yh).defined(def),
  areahShape = area$2$1().y(y).x1(x).x0(xw).defined(def),
  lineShape = line$3().x(x).y(y).defined(def),
  rectShape = vg_rect().x(x).y(y).width(w$1).height(h).cornerRadius(tl, tr, br, bl),
  symbolShape = Symbol$1().type(type$1).size(sz),
  trailShape = vg_trail().x(x).y(y).defined(def).size(ts);
function hasCornerRadius(item) {
  return item.cornerRadius || item.cornerRadiusTopLeft || item.cornerRadiusTopRight || item.cornerRadiusBottomRight || item.cornerRadiusBottomLeft;
}
function arc$1$1(context, item) {
  return arcShape.context(context)(item);
}
function area$1$1(context, items) {
  const item = items[0],
    interp = item.interpolate || 'linear';
  return (item.orient === 'horizontal' ? areahShape : areavShape).curve(curves(interp, item.orient, item.tension)).context(context)(items);
}
function line$1$1(context, items) {
  const item = items[0],
    interp = item.interpolate || 'linear';
  return lineShape.curve(curves(interp, item.orient, item.tension)).context(context)(items);
}
function rectangle(context, item, x, y) {
  return rectShape.context(context)(item, x, y);
}
function shape$1(context, item) {
  return (item.mark.shape || item.shape).context(context)(item);
}
function symbol$1(context, item) {
  return symbolShape.context(context)(item);
}
function trail$1(context, items) {
  return trailShape.context(context)(items);
}

var clip_id = 1;
function resetSVGClipId() {
  clip_id = 1;
}
function clip$1 (renderer, item, size) {
  var clip = item.clip,
    defs = renderer._defs,
    id = item.clip_id || (item.clip_id = 'clip' + clip_id++),
    c = defs.clipping[id] || (defs.clipping[id] = {
      id: id
    });
  if (isFunction(clip)) {
    c.path = clip(null);
  } else if (hasCornerRadius(size)) {
    c.path = rectangle(null, size, 0, 0);
  } else {
    c.width = size.width || 0;
    c.height = size.height || 0;
  }
  return 'url(#' + id + ')';
}

function Bounds(b) {
  this.clear();
  if (b) this.union(b);
}
Bounds.prototype = {
  clone() {
    return new Bounds(this);
  },
  clear() {
    this.x1 = +Number.MAX_VALUE;
    this.y1 = +Number.MAX_VALUE;
    this.x2 = -Number.MAX_VALUE;
    this.y2 = -Number.MAX_VALUE;
    return this;
  },
  empty() {
    return this.x1 === +Number.MAX_VALUE && this.y1 === +Number.MAX_VALUE && this.x2 === -Number.MAX_VALUE && this.y2 === -Number.MAX_VALUE;
  },
  equals(b) {
    return this.x1 === b.x1 && this.y1 === b.y1 && this.x2 === b.x2 && this.y2 === b.y2;
  },
  set(x1, y1, x2, y2) {
    if (x2 < x1) {
      this.x2 = x1;
      this.x1 = x2;
    } else {
      this.x1 = x1;
      this.x2 = x2;
    }
    if (y2 < y1) {
      this.y2 = y1;
      this.y1 = y2;
    } else {
      this.y1 = y1;
      this.y2 = y2;
    }
    return this;
  },
  add(x, y) {
    if (x < this.x1) this.x1 = x;
    if (y < this.y1) this.y1 = y;
    if (x > this.x2) this.x2 = x;
    if (y > this.y2) this.y2 = y;
    return this;
  },
  expand(d) {
    this.x1 -= d;
    this.y1 -= d;
    this.x2 += d;
    this.y2 += d;
    return this;
  },
  round() {
    this.x1 = Math.floor(this.x1);
    this.y1 = Math.floor(this.y1);
    this.x2 = Math.ceil(this.x2);
    this.y2 = Math.ceil(this.y2);
    return this;
  },
  scale(s) {
    this.x1 *= s;
    this.y1 *= s;
    this.x2 *= s;
    this.y2 *= s;
    return this;
  },
  translate(dx, dy) {
    this.x1 += dx;
    this.x2 += dx;
    this.y1 += dy;
    this.y2 += dy;
    return this;
  },
  rotate(angle, x, y) {
    const p = this.rotatedPoints(angle, x, y);
    return this.clear().add(p[0], p[1]).add(p[2], p[3]).add(p[4], p[5]).add(p[6], p[7]);
  },
  rotatedPoints(angle, x, y) {
    var {
        x1,
        y1,
        x2,
        y2
      } = this,
      cos = Math.cos(angle),
      sin = Math.sin(angle),
      cx = x - x * cos + y * sin,
      cy = y - x * sin - y * cos;
    return [cos * x1 - sin * y1 + cx, sin * x1 + cos * y1 + cy, cos * x1 - sin * y2 + cx, sin * x1 + cos * y2 + cy, cos * x2 - sin * y1 + cx, sin * x2 + cos * y1 + cy, cos * x2 - sin * y2 + cx, sin * x2 + cos * y2 + cy];
  },
  union(b) {
    if (b.x1 < this.x1) this.x1 = b.x1;
    if (b.y1 < this.y1) this.y1 = b.y1;
    if (b.x2 > this.x2) this.x2 = b.x2;
    if (b.y2 > this.y2) this.y2 = b.y2;
    return this;
  },
  intersect(b) {
    if (b.x1 > this.x1) this.x1 = b.x1;
    if (b.y1 > this.y1) this.y1 = b.y1;
    if (b.x2 < this.x2) this.x2 = b.x2;
    if (b.y2 < this.y2) this.y2 = b.y2;
    return this;
  },
  encloses(b) {
    return b && this.x1 <= b.x1 && this.x2 >= b.x2 && this.y1 <= b.y1 && this.y2 >= b.y2;
  },
  alignsWith(b) {
    return b && (this.x1 == b.x1 || this.x2 == b.x2 || this.y1 == b.y1 || this.y2 == b.y2);
  },
  intersects(b) {
    return b && !(this.x2 < b.x1 || this.x1 > b.x2 || this.y2 < b.y1 || this.y1 > b.y2);
  },
  contains(x, y) {
    return !(x < this.x1 || x > this.x2 || y < this.y1 || y > this.y2);
  },
  width() {
    return this.x2 - this.x1;
  },
  height() {
    return this.y2 - this.y1;
  }
};

function Item(mark) {
  this.mark = mark;
  this.bounds = this.bounds || new Bounds();
}

function GroupItem(mark) {
  Item.call(this, mark);
  this.items = this.items || [];
}
inherits(GroupItem, Item);

class ResourceLoader {
  constructor(customLoader) {
    this._pending = 0;
    this._loader = customLoader || loader();
  }
  pending() {
    return this._pending;
  }
  sanitizeURL(uri) {
    const loader = this;
    increment(loader);
    return loader._loader.sanitize(uri, {
      context: 'href'
    }).then(opt => {
      decrement(loader);
      return opt;
    }).catch(() => {
      decrement(loader);
      return null;
    });
  }
  loadImage(uri) {
    const loader = this,
      Image = domImage();
    increment(loader);
    return loader._loader.sanitize(uri, {
      context: 'image'
    }).then(opt => {
      const url = opt.href;
      if (!url || !Image) throw {
        url: url
      };
      const img = new Image();

      // set crossOrigin only if cors is defined; empty string sets anonymous mode
      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/crossOrigin
      const cors = has$1(opt, 'crossOrigin') ? opt.crossOrigin : 'anonymous';
      if (cors != null) img.crossOrigin = cors;

      // attempt to load image resource
      img.onload = () => decrement(loader);
      img.onerror = () => decrement(loader);
      img.src = url;
      return img;
    }).catch(e => {
      decrement(loader);
      return {
        complete: false,
        width: 0,
        height: 0,
        src: e && e.url || ''
      };
    });
  }
  ready() {
    const loader = this;
    return new Promise(accept => {
      function poll(value) {
        if (!loader.pending()) accept(value);else setTimeout(() => {
          poll(true);
        }, 10);
      }
      poll(false);
    });
  }
}
function increment(loader) {
  loader._pending += 1;
}
function decrement(loader) {
  loader._pending -= 1;
}

function boundStroke (bounds, item, miter) {
  if (item.stroke && item.opacity !== 0 && item.strokeOpacity !== 0) {
    const sw = item.strokeWidth != null ? +item.strokeWidth : 1;
    bounds.expand(sw + (miter ? miterAdjustment(item, sw) : 0));
  }
  return bounds;
}
function miterAdjustment(item, strokeWidth) {
  // TODO: more sophisticated adjustment? Or miter support in boundContext?
  return item.strokeJoin && item.strokeJoin !== 'miter' ? 0 : strokeWidth;
}

const circleThreshold = Tau - 1e-8;
let bounds, lx, ly, rot, ma, mb, mc, md;
const add$1 = (x, y) => bounds.add(x, y);
const addL = (x, y) => add$1(lx = x, ly = y);
const addX = x => add$1(x, bounds.y1);
const addY = y => add$1(bounds.x1, y);
const px = (x, y) => ma * x + mc * y;
const py = (x, y) => mb * x + md * y;
const addp = (x, y) => add$1(px(x, y), py(x, y));
const addpL = (x, y) => addL(px(x, y), py(x, y));
function boundContext (_, deg) {
  bounds = _;
  if (deg) {
    rot = deg * DegToRad;
    ma = md = Math.cos(rot);
    mb = Math.sin(rot);
    mc = -mb;
  } else {
    ma = md = 1;
    rot = mb = mc = 0;
  }
  return context$1;
}
const context$1 = {
  beginPath() {},
  closePath() {},
  moveTo: addpL,
  lineTo: addpL,
  rect(x, y, w, h) {
    if (rot) {
      addp(x + w, y);
      addp(x + w, y + h);
      addp(x, y + h);
      addpL(x, y);
    } else {
      add$1(x + w, y + h);
      addL(x, y);
    }
  },
  quadraticCurveTo(x1, y1, x2, y2) {
    const px1 = px(x1, y1),
      py1 = py(x1, y1),
      px2 = px(x2, y2),
      py2 = py(x2, y2);
    quadExtrema(lx, px1, px2, addX);
    quadExtrema(ly, py1, py2, addY);
    addL(px2, py2);
  },
  bezierCurveTo(x1, y1, x2, y2, x3, y3) {
    const px1 = px(x1, y1),
      py1 = py(x1, y1),
      px2 = px(x2, y2),
      py2 = py(x2, y2),
      px3 = px(x3, y3),
      py3 = py(x3, y3);
    cubicExtrema(lx, px1, px2, px3, addX);
    cubicExtrema(ly, py1, py2, py3, addY);
    addL(px3, py3);
  },
  arc(cx, cy, r, sa, ea, ccw) {
    sa += rot;
    ea += rot;

    // store last point on path
    lx = r * Math.cos(ea) + cx;
    ly = r * Math.sin(ea) + cy;
    if (Math.abs(ea - sa) > circleThreshold) {
      // treat as full circle
      add$1(cx - r, cy - r);
      add$1(cx + r, cy + r);
    } else {
      const update = a => add$1(r * Math.cos(a) + cx, r * Math.sin(a) + cy);
      let s, i;

      // sample end points
      update(sa);
      update(ea);

      // sample interior points aligned with 90 degrees
      if (ea !== sa) {
        sa = sa % Tau;
        if (sa < 0) sa += Tau;
        ea = ea % Tau;
        if (ea < 0) ea += Tau;
        if (ea < sa) {
          ccw = !ccw; // flip direction
          s = sa;
          sa = ea;
          ea = s; // swap end-points
        }
        if (ccw) {
          ea -= Tau;
          s = sa - sa % HalfPi;
          for (i = 0; i < 4 && s > ea; ++i, s -= HalfPi) update(s);
        } else {
          s = sa - sa % HalfPi + HalfPi;
          for (i = 0; i < 4 && s < ea; ++i, s = s + HalfPi) update(s);
        }
      }
    }
  }
};
function quadExtrema(x0, x1, x2, cb) {
  const t = (x0 - x1) / (x0 + x2 - 2 * x1);
  if (0 < t && t < 1) cb(x0 + (x1 - x0) * t);
}
function cubicExtrema(x0, x1, x2, x3, cb) {
  const a = x3 - x0 + 3 * x1 - 3 * x2,
    b = x0 + x2 - 2 * x1,
    c = x0 - x1;
  let t0 = 0,
    t1 = 0,
    r;

  // solve for parameter t
  if (Math.abs(a) > Epsilon) {
    // quadratic equation
    r = b * b + c * a;
    if (r >= 0) {
      r = Math.sqrt(r);
      t0 = (-b + r) / a;
      t1 = (-b - r) / a;
    }
  } else {
    // linear equation
    t0 = 0.5 * c / b;
  }

  // calculate position
  if (0 < t0 && t0 < 1) cb(cubic(t0, x0, x1, x2, x3));
  if (0 < t1 && t1 < 1) cb(cubic(t1, x0, x1, x2, x3));
}
function cubic(t, x0, x1, x2, x3) {
  const s = 1 - t,
    s2 = s * s,
    t2 = t * t;
  return s2 * s * x0 + 3 * s2 * t * x1 + 3 * s * t2 * x2 + t2 * t * x3;
}

var context$2 = (context$2 = domCanvas(1, 1)) ? context$2.getContext('2d') : null;

const b = new Bounds();
function intersectPath(draw) {
  return function (item, brush) {
    // rely on (inaccurate) bounds intersection if no context
    if (!context$2) return true;

    // add path to offscreen graphics context
    draw(context$2, item);

    // get bounds intersection region
    b.clear().union(item.bounds).intersect(brush).round();
    const {
      x1,
      y1,
      x2,
      y2
    } = b;

    // iterate over intersection region
    // perform fine grained inclusion test
    for (let y = y1; y <= y2; ++y) {
      for (let x = x1; x <= x2; ++x) {
        if (context$2.isPointInPath(x, y)) {
          return true;
        }
      }
    }

    // false if no hits in intersection region
    return false;
  };
}
function intersectPoint(item, box) {
  return box.contains(item.x || 0, item.y || 0);
}
function intersectRect(item, box) {
  const x = item.x || 0,
    y = item.y || 0,
    w = item.width || 0,
    h = item.height || 0;
  return box.intersects(b.set(x, y, x + w, y + h));
}
function intersectRule(item, box) {
  const x = item.x || 0,
    y = item.y || 0,
    x2 = item.x2 != null ? item.x2 : x,
    y2 = item.y2 != null ? item.y2 : y;
  return intersectBoxLine(box, x, y, x2, y2);
}
function intersectBoxLine(box, x, y, u, v) {
  const {
      x1,
      y1,
      x2,
      y2
    } = box,
    dx = u - x,
    dy = v - y;
  let t0 = 0,
    t1 = 1,
    p,
    q,
    r,
    e;
  for (e = 0; e < 4; ++e) {
    if (e === 0) {
      p = -dx;
      q = -(x1 - x);
    }
    if (e === 1) {
      p = dx;
      q = x2 - x;
    }
    if (e === 2) {
      p = -dy;
      q = -(y1 - y);
    }
    if (e === 3) {
      p = dy;
      q = y2 - y;
    }
    if (Math.abs(p) < 1e-10 && q < 0) return false;
    r = q / p;
    if (p < 0) {
      if (r > t1) return false;else if (r > t0) t0 = r;
    } else if (p > 0) {
      if (r < t0) return false;else if (r < t1) t1 = r;
    }
  }
  return true;
}

function blend (context, item) {
  context.globalCompositeOperation = item.blend || 'source-over';
}

function value$2 (value, dflt) {
  return value == null ? dflt : value;
}

function addStops(gradient, stops) {
  const n = stops.length;
  for (let i = 0; i < n; ++i) {
    gradient.addColorStop(stops[i].offset, stops[i].color);
  }
  return gradient;
}
function gradient$2 (context, spec, bounds) {
  const w = bounds.width(),
    h = bounds.height();
  let gradient;
  if (spec.gradient === 'radial') {
    gradient = context.createRadialGradient(bounds.x1 + value$2(spec.x1, 0.5) * w, bounds.y1 + value$2(spec.y1, 0.5) * h, Math.max(w, h) * value$2(spec.r1, 0), bounds.x1 + value$2(spec.x2, 0.5) * w, bounds.y1 + value$2(spec.y2, 0.5) * h, Math.max(w, h) * value$2(spec.r2, 0.5));
  } else {
    // linear gradient
    const x1 = value$2(spec.x1, 0),
      y1 = value$2(spec.y1, 0),
      x2 = value$2(spec.x2, 1),
      y2 = value$2(spec.y2, 0);
    if (x1 === x2 || y1 === y2 || w === h) {
      // axis aligned: use normal gradient
      gradient = context.createLinearGradient(bounds.x1 + x1 * w, bounds.y1 + y1 * h, bounds.x1 + x2 * w, bounds.y1 + y2 * h);
    } else {
      // not axis aligned: render gradient into a pattern (#2365)
      // this allows us to use normalized bounding box coordinates
      const image = domCanvas(Math.ceil(w), Math.ceil(h)),
        ictx = image.getContext('2d');
      ictx.scale(w, h);
      ictx.fillStyle = addStops(ictx.createLinearGradient(x1, y1, x2, y2), spec.stops);
      ictx.fillRect(0, 0, w, h);
      return context.createPattern(image, 'no-repeat');
    }
  }
  return addStops(gradient, spec.stops);
}

function color$2 (context, item, value) {
  return isGradient(value) ? gradient$2(context, value, item.bounds) : value;
}

function fill (context, item, opacity) {
  opacity *= item.fillOpacity == null ? 1 : item.fillOpacity;
  if (opacity > 0) {
    context.globalAlpha = opacity;
    context.fillStyle = color$2(context, item, item.fill);
    return true;
  } else {
    return false;
  }
}

var Empty = [];
function stroke (context, item, opacity) {
  var lw = (lw = item.strokeWidth) != null ? lw : 1;
  if (lw <= 0) return false;
  opacity *= item.strokeOpacity == null ? 1 : item.strokeOpacity;
  if (opacity > 0) {
    context.globalAlpha = opacity;
    context.strokeStyle = color$2(context, item, item.stroke);
    context.lineWidth = lw;
    context.lineCap = item.strokeCap || 'butt';
    context.lineJoin = item.strokeJoin || 'miter';
    context.miterLimit = item.strokeMiterLimit || 10;
    if (context.setLineDash) {
      context.setLineDash(item.strokeDash || Empty);
      context.lineDashOffset = item.strokeDashOffset || 0;
    }
    return true;
  } else {
    return false;
  }
}

function compare$1(a, b) {
  return a.zindex - b.zindex || a.index - b.index;
}
function zorder(scene) {
  if (!scene.zdirty) return scene.zitems;
  var items = scene.items,
    output = [],
    item,
    i,
    n;
  for (i = 0, n = items.length; i < n; ++i) {
    item = items[i];
    item.index = i;
    if (item.zindex) output.push(item);
  }
  scene.zdirty = false;
  return scene.zitems = output.sort(compare$1);
}
function visit(scene, visitor) {
  var items = scene.items,
    i,
    n;
  if (!items || !items.length) return;
  const zitems = zorder(scene);
  if (zitems && zitems.length) {
    for (i = 0, n = items.length; i < n; ++i) {
      if (!items[i].zindex) visitor(items[i]);
    }
    items = zitems;
  }
  for (i = 0, n = items.length; i < n; ++i) {
    visitor(items[i]);
  }
}
function pickVisit(scene, visitor) {
  var items = scene.items,
    hit,
    i;
  if (!items || !items.length) return null;
  const zitems = zorder(scene);
  if (zitems && zitems.length) items = zitems;
  for (i = items.length; --i >= 0;) {
    if (hit = visitor(items[i])) return hit;
  }
  if (items === zitems) {
    for (items = scene.items, i = items.length; --i >= 0;) {
      if (!items[i].zindex) {
        if (hit = visitor(items[i])) return hit;
      }
    }
  }
  return null;
}

function drawAll(path) {
  return function (context, scene, bounds) {
    visit(scene, item => {
      if (!bounds || bounds.intersects(item.bounds)) {
        drawPath(path, context, item, item);
      }
    });
  };
}
function drawOne(path) {
  return function (context, scene, bounds) {
    if (scene.items.length && (!bounds || bounds.intersects(scene.bounds))) {
      drawPath(path, context, scene.items[0], scene.items);
    }
  };
}
function drawPath(path, context, item, items) {
  var opacity = item.opacity == null ? 1 : item.opacity;
  if (opacity === 0) return;
  if (path(context, items)) return;
  blend(context, item);
  if (item.fill && fill(context, item, opacity)) {
    context.fill();
  }
  if (item.stroke && stroke(context, item, opacity)) {
    context.stroke();
  }
}

function pick$1(test) {
  test = test || truthy;
  return function (context, scene, x, y, gx, gy) {
    x *= context.pixelRatio;
    y *= context.pixelRatio;
    return pickVisit(scene, item => {
      const b = item.bounds;
      // first hit test against bounding box
      if (b && !b.contains(gx, gy) || !b) return;
      // if in bounding box, perform more careful test
      if (test(context, item, x, y, gx, gy)) return item;
    });
  };
}
function hitPath(path, filled) {
  return function (context, o, x, y) {
    var item = Array.isArray(o) ? o[0] : o,
      fill = filled == null ? item.fill : filled,
      stroke = item.stroke && context.isPointInStroke,
      lw,
      lc;
    if (stroke) {
      lw = item.strokeWidth;
      lc = item.strokeCap;
      context.lineWidth = lw != null ? lw : 1;
      context.lineCap = lc != null ? lc : 'butt';
    }
    return path(context, o) ? false : fill && context.isPointInPath(x, y) || stroke && context.isPointInStroke(x, y);
  };
}
function pickPath(path) {
  return pick$1(hitPath(path));
}

function translate$2(x, y) {
  return 'translate(' + x + ',' + y + ')';
}
function rotate(a) {
  return 'rotate(' + a + ')';
}
function scale$3(scaleX, scaleY) {
  return 'scale(' + scaleX + ',' + scaleY + ')';
}
function translateItem(item) {
  return translate$2(item.x || 0, item.y || 0);
}
function rotateItem(item) {
  return translate$2(item.x || 0, item.y || 0) + (item.angle ? ' ' + rotate(item.angle) : '');
}
function transformItem(item) {
  return translate$2(item.x || 0, item.y || 0) + (item.angle ? ' ' + rotate(item.angle) : '') + (item.scaleX || item.scaleY ? ' ' + scale$3(item.scaleX || 1, item.scaleY || 1) : '');
}

function markItemPath (type, shape, isect) {
  function attr(emit, item) {
    emit('transform', rotateItem(item));
    emit('d', shape(null, item));
  }
  function bound(bounds, item) {
    shape(boundContext(bounds, item.angle), item);
    return boundStroke(bounds, item).translate(item.x || 0, item.y || 0);
  }
  function draw(context, item) {
    var x = item.x || 0,
      y = item.y || 0,
      a = item.angle || 0;
    context.translate(x, y);
    if (a) context.rotate(a *= DegToRad);
    context.beginPath();
    shape(context, item);
    if (a) context.rotate(-a);
    context.translate(-x, -y);
  }
  return {
    type: type,
    tag: 'path',
    nested: false,
    attr: attr,
    bound: bound,
    draw: drawAll(draw),
    pick: pickPath(draw),
    isect: isect || intersectPath(draw)
  };
}

var arc$2 = markItemPath('arc', arc$1$1);

function pickArea(a, p) {
  var v = a[0].orient === 'horizontal' ? p[1] : p[0],
    z = a[0].orient === 'horizontal' ? 'y' : 'x',
    i = a.length,
    min = +Infinity,
    hit,
    d;
  while (--i >= 0) {
    if (a[i].defined === false) continue;
    d = Math.abs(a[i][z] - v);
    if (d < min) {
      min = d;
      hit = a[i];
    }
  }
  return hit;
}
function pickLine(a, p) {
  var t = Math.pow(a[0].strokeWidth || 1, 2),
    i = a.length,
    dx,
    dy,
    dd;
  while (--i >= 0) {
    if (a[i].defined === false) continue;
    dx = a[i].x - p[0];
    dy = a[i].y - p[1];
    dd = dx * dx + dy * dy;
    if (dd < t) return a[i];
  }
  return null;
}
function pickTrail(a, p) {
  var i = a.length,
    dx,
    dy,
    dd;
  while (--i >= 0) {
    if (a[i].defined === false) continue;
    dx = a[i].x - p[0];
    dy = a[i].y - p[1];
    dd = dx * dx + dy * dy;
    dx = a[i].size || 1;
    if (dd < dx * dx) return a[i];
  }
  return null;
}

function markMultiItemPath (type, shape, tip) {
  function attr(emit, item) {
    var items = item.mark.items;
    if (items.length) emit('d', shape(null, items));
  }
  function bound(bounds, mark) {
    var items = mark.items;
    if (items.length === 0) {
      return bounds;
    } else {
      shape(boundContext(bounds), items);
      return boundStroke(bounds, items[0]);
    }
  }
  function draw(context, items) {
    context.beginPath();
    shape(context, items);
  }
  const hit = hitPath(draw);
  function pick(context, scene, x, y, gx, gy) {
    var items = scene.items,
      b = scene.bounds;
    if (!items || !items.length || b && !b.contains(gx, gy)) {
      return null;
    }
    x *= context.pixelRatio;
    y *= context.pixelRatio;
    return hit(context, items, x, y) ? items[0] : null;
  }
  return {
    type: type,
    tag: 'path',
    nested: true,
    attr: attr,
    bound: bound,
    draw: drawOne(draw),
    pick: pick,
    isect: intersectPoint,
    tip: tip
  };
}

var area$2 = markMultiItemPath('area', area$1$1, pickArea);

function clip$2 (context, scene) {
  var clip = scene.clip;
  context.save();
  if (isFunction(clip)) {
    context.beginPath();
    clip(context);
    context.clip();
  } else {
    clipGroup(context, scene.group);
  }
}
function clipGroup(context, group) {
  context.beginPath();
  hasCornerRadius(group) ? rectangle(context, group, 0, 0) : context.rect(0, 0, group.width || 0, group.height || 0);
  context.clip();
}

function offset$1(item) {
  const sw = value$2(item.strokeWidth, 1);
  return item.strokeOffset != null ? item.strokeOffset : item.stroke && sw > 0.5 && sw < 1.5 ? 0.5 - Math.abs(sw - 1) : 0;
}
function attr$5(emit, item) {
  emit('transform', translateItem(item));
}
function emitRectangle(emit, item) {
  const off = offset$1(item);
  emit('d', rectangle(null, item, off, off));
}
function background$1(emit, item) {
  emit('class', 'background');
  emit('aria-hidden', true);
  emitRectangle(emit, item);
}
function foreground(emit, item) {
  emit('class', 'foreground');
  emit('aria-hidden', true);
  if (item.strokeForeground) {
    emitRectangle(emit, item);
  } else {
    emit('d', '');
  }
}
function content(emit, item, renderer) {
  const url = item.clip ? clip$1(renderer, item, item) : null;
  emit('clip-path', url);
}
function bound$5(bounds, group) {
  if (!group.clip && group.items) {
    const items = group.items,
      m = items.length;
    for (let j = 0; j < m; ++j) {
      bounds.union(items[j].bounds);
    }
  }
  if ((group.clip || group.width || group.height) && !group.noBound) {
    bounds.add(0, 0).add(group.width || 0, group.height || 0);
  }
  boundStroke(bounds, group);
  return bounds.translate(group.x || 0, group.y || 0);
}
function rectanglePath(context, group, x, y) {
  const off = offset$1(group);
  context.beginPath();
  rectangle(context, group, (x || 0) + off, (y || 0) + off);
}
const hitBackground = hitPath(rectanglePath);
const hitForeground = hitPath(rectanglePath, false);
const hitCorner = hitPath(rectanglePath, true);
function draw$4(context, scene, bounds, markTypes) {
  visit(scene, group => {
    const gx = group.x || 0,
      gy = group.y || 0,
      fore = group.strokeForeground,
      opacity = group.opacity == null ? 1 : group.opacity;

    // draw group background
    if ((group.stroke || group.fill) && opacity) {
      rectanglePath(context, group, gx, gy);
      blend(context, group);
      if (group.fill && fill(context, group, opacity)) {
        context.fill();
      }
      if (group.stroke && !fore && stroke(context, group, opacity)) {
        context.stroke();
      }
    }

    // setup graphics context, set clip and bounds
    context.save();
    context.translate(gx, gy);
    if (group.clip) clipGroup(context, group);
    if (bounds) bounds.translate(-gx, -gy);

    // draw group contents
    visit(group, item => {
      if (item.marktype === 'group' || markTypes == null || markTypes.includes(item.marktype)) {
        this.draw(context, item, bounds, markTypes);
      }
    });

    // restore graphics context
    if (bounds) bounds.translate(gx, gy);
    context.restore();

    // draw group foreground
    if (fore && group.stroke && opacity) {
      rectanglePath(context, group, gx, gy);
      blend(context, group);
      if (stroke(context, group, opacity)) {
        context.stroke();
      }
    }
  });
}
function pick$2(context, scene, x, y, gx, gy) {
  if (scene.bounds && !scene.bounds.contains(gx, gy) || !scene.items) {
    return null;
  }
  const cx = x * context.pixelRatio,
    cy = y * context.pixelRatio;
  return pickVisit(scene, group => {
    let hit, dx, dy;

    // first hit test bounding box
    const b = group.bounds;
    if (b && !b.contains(gx, gy)) return;

    // passed bounds check, test rectangular clip
    dx = group.x || 0;
    dy = group.y || 0;
    const dw = dx + (group.width || 0),
      dh = dy + (group.height || 0),
      c = group.clip;
    if (c && (gx < dx || gx > dw || gy < dy || gy > dh)) return;

    // adjust coordinate system
    context.save();
    context.translate(dx, dy);
    dx = gx - dx;
    dy = gy - dy;

    // test background for rounded corner clip
    if (c && hasCornerRadius(group) && !hitCorner(context, group, cx, cy)) {
      context.restore();
      return null;
    }
    const fore = group.strokeForeground,
      ix = scene.interactive !== false;

    // hit test against group foreground
    if (ix && fore && group.stroke && hitForeground(context, group, cx, cy)) {
      context.restore();
      return group;
    }

    // hit test against contained marks
    hit = pickVisit(group, mark => pickMark(mark, dx, dy) ? this.pick(mark, x, y, dx, dy) : null);

    // hit test against group background
    if (!hit && ix && (group.fill || !fore && group.stroke) && hitBackground(context, group, cx, cy)) {
      hit = group;
    }

    // restore state and return
    context.restore();
    return hit || null;
  });
}
function pickMark(mark, x, y) {
  return (mark.interactive !== false || mark.marktype === 'group') && mark.bounds && mark.bounds.contains(x, y);
}
var group = {
  type: 'group',
  tag: 'g',
  nested: false,
  attr: attr$5,
  bound: bound$5,
  draw: draw$4,
  pick: pick$2,
  isect: intersectRect,
  content: content,
  background: background$1,
  foreground: foreground
};

var metadata = {
  'xmlns': 'http://www.w3.org/2000/svg',
  'xmlns:xlink': 'http://www.w3.org/1999/xlink',
  'version': '1.1'
};

function getImage(item, renderer) {
  var image = item.image;
  if (!image || item.url && item.url !== image.url) {
    image = {
      complete: false,
      width: 0,
      height: 0
    };
    renderer.loadImage(item.url).then(image => {
      item.image = image;
      item.image.url = item.url;
    });
  }
  return image;
}
function imageWidth(item, image) {
  return item.width != null ? item.width : !image || !image.width ? 0 : item.aspect !== false && item.height ? item.height * image.width / image.height : image.width;
}
function imageHeight(item, image) {
  return item.height != null ? item.height : !image || !image.height ? 0 : item.aspect !== false && item.width ? item.width * image.height / image.width : image.height;
}
function imageXOffset(align, w) {
  return align === 'center' ? w / 2 : align === 'right' ? w : 0;
}
function imageYOffset(baseline, h) {
  return baseline === 'middle' ? h / 2 : baseline === 'bottom' ? h : 0;
}
function attr$4(emit, item, renderer) {
  const img = getImage(item, renderer),
    w = imageWidth(item, img),
    h = imageHeight(item, img),
    x = (item.x || 0) - imageXOffset(item.align, w),
    y = (item.y || 0) - imageYOffset(item.baseline, h),
    i = !img.src && img.toDataURL ? img.toDataURL() : img.src || '';
  emit('href', i, metadata['xmlns:xlink'], 'xlink:href');
  emit('transform', translate$2(x, y));
  emit('width', w);
  emit('height', h);
  emit('preserveAspectRatio', item.aspect === false ? 'none' : 'xMidYMid');
}
function bound$4(bounds, item) {
  const img = item.image,
    w = imageWidth(item, img),
    h = imageHeight(item, img),
    x = (item.x || 0) - imageXOffset(item.align, w),
    y = (item.y || 0) - imageYOffset(item.baseline, h);
  return bounds.set(x, y, x + w, y + h);
}
function draw$3(context, scene, bounds) {
  visit(scene, item => {
    if (bounds && !bounds.intersects(item.bounds)) return; // bounds check

    const img = getImage(item, this);
    let w = imageWidth(item, img);
    let h = imageHeight(item, img);
    if (w === 0 || h === 0) return; // early exit

    let x = (item.x || 0) - imageXOffset(item.align, w),
      y = (item.y || 0) - imageYOffset(item.baseline, h),
      opacity,
      ar0,
      ar1,
      t;
    if (item.aspect !== false) {
      ar0 = img.width / img.height;
      ar1 = item.width / item.height;
      if (ar0 === ar0 && ar1 === ar1 && ar0 !== ar1) {
        if (ar1 < ar0) {
          t = w / ar0;
          y += (h - t) / 2;
          h = t;
        } else {
          t = h * ar0;
          x += (w - t) / 2;
          w = t;
        }
      }
    }
    if (img.complete || img.toDataURL) {
      blend(context, item);
      context.globalAlpha = (opacity = item.opacity) != null ? opacity : 1;
      context.imageSmoothingEnabled = item.smooth !== false;
      context.drawImage(img, x, y, w, h);
    }
  });
}
var image$1 = {
  type: 'image',
  tag: 'image',
  nested: false,
  attr: attr$4,
  bound: bound$4,
  draw: draw$3,
  pick: pick$1(),
  isect: truthy,
  // bounds check is sufficient
  get: getImage,
  xOffset: imageXOffset,
  yOffset: imageYOffset
};

var line$2 = markMultiItemPath('line', line$1$1, pickLine);

function attr$3(emit, item) {
  var sx = item.scaleX || 1,
    sy = item.scaleY || 1;
  if (sx !== 1 || sy !== 1) {
    emit('vector-effect', 'non-scaling-stroke');
  }
  emit('transform', transformItem(item));
  emit('d', item.path);
}
function path$1(context, item) {
  var path = item.path;
  if (path == null) return true;
  var x = item.x || 0,
    y = item.y || 0,
    sx = item.scaleX || 1,
    sy = item.scaleY || 1,
    a = (item.angle || 0) * DegToRad,
    cache = item.pathCache;
  if (!cache || cache.path !== path) {
    (item.pathCache = cache = parse$3(path)).path = path;
  }
  if (a && context.rotate && context.translate) {
    context.translate(x, y);
    context.rotate(a);
    pathRender(context, cache, 0, 0, sx, sy);
    context.rotate(-a);
    context.translate(-x, -y);
  } else {
    pathRender(context, cache, x, y, sx, sy);
  }
}
function bound$3(bounds, item) {
  return path$1(boundContext(bounds, item.angle), item) ? bounds.set(0, 0, 0, 0) : boundStroke(bounds, item, true);
}
var path$2 = {
  type: 'path',
  tag: 'path',
  nested: false,
  attr: attr$3,
  bound: bound$3,
  draw: drawAll(path$1),
  pick: pickPath(path$1),
  isect: intersectPath(path$1)
};

function attr$2(emit, item) {
  emit('d', rectangle(null, item));
}
function bound$2(bounds, item) {
  var x, y;
  return boundStroke(bounds.set(x = item.x || 0, y = item.y || 0, x + item.width || 0, y + item.height || 0), item);
}
function draw$2(context, item) {
  context.beginPath();
  rectangle(context, item);
}
var rect$1 = {
  type: 'rect',
  tag: 'path',
  nested: false,
  attr: attr$2,
  bound: bound$2,
  draw: drawAll(draw$2),
  pick: pickPath(draw$2),
  isect: intersectRect
};

function attr$1(emit, item) {
  emit('transform', translateItem(item));
  emit('x2', item.x2 != null ? item.x2 - (item.x || 0) : 0);
  emit('y2', item.y2 != null ? item.y2 - (item.y || 0) : 0);
}
function bound$1(bounds, item) {
  var x1, y1;
  return boundStroke(bounds.set(x1 = item.x || 0, y1 = item.y || 0, item.x2 != null ? item.x2 : x1, item.y2 != null ? item.y2 : y1), item);
}
function path(context, item, opacity) {
  var x1, y1, x2, y2;
  if (item.stroke && stroke(context, item, opacity)) {
    x1 = item.x || 0;
    y1 = item.y || 0;
    x2 = item.x2 != null ? item.x2 : x1;
    y2 = item.y2 != null ? item.y2 : y1;
    context.beginPath();
    context.moveTo(x1, y1);
    context.lineTo(x2, y2);
    return true;
  }
  return false;
}
function draw$1(context, scene, bounds) {
  visit(scene, item => {
    if (bounds && !bounds.intersects(item.bounds)) return; // bounds check
    var opacity = item.opacity == null ? 1 : item.opacity;
    if (opacity && path(context, item, opacity)) {
      blend(context, item);
      context.stroke();
    }
  });
}
function hit$1(context, item, x, y) {
  if (!context.isPointInStroke) return false;
  return path(context, item, 1) && context.isPointInStroke(x, y);
}
var rule$2 = {
  type: 'rule',
  tag: 'line',
  nested: false,
  attr: attr$1,
  bound: bound$1,
  draw: draw$1,
  pick: pick$1(hit$1),
  isect: intersectRule
};

var shape = markItemPath('shape', shape$1);

var symbol = markItemPath('symbol', symbol$1, intersectPoint);

// memoize text width measurement
const widthCache = lruCache();
var textMetrics = {
  height: fontSize,
  measureWidth: measureWidth,
  estimateWidth: estimateWidth,
  width: estimateWidth,
  canvas: useCanvas
};
useCanvas(true);
function useCanvas(use) {
  textMetrics.width = use && context$2 ? measureWidth : estimateWidth;
}

// make simple estimate if no canvas is available
function estimateWidth(item, text) {
  return _estimateWidth(textValue(item, text), fontSize(item));
}
function _estimateWidth(text, currentFontHeight) {
  return ~~(0.8 * text.length * currentFontHeight);
}

// measure text width if canvas is available
function measureWidth(item, text) {
  return fontSize(item) <= 0 || !(text = textValue(item, text)) ? 0 : _measureWidth(text, font$1(item));
}
function _measureWidth(text, currentFont) {
  const key = `(${currentFont}) ${text}`;
  let width = widthCache.get(key);
  if (width === undefined) {
    context$2.font = currentFont;
    width = context$2.measureText(text).width;
    widthCache.set(key, width);
  }
  return width;
}
function fontSize(item) {
  return item.fontSize != null ? +item.fontSize || 0 : 11;
}
function lineHeight(item) {
  return item.lineHeight != null ? item.lineHeight : fontSize(item) + 2;
}
function lineArray(_) {
  return isArray(_) ? _.length > 1 ? _ : _[0] : _;
}
function textLines(item) {
  return lineArray(item.lineBreak && item.text && !isArray(item.text) ? item.text.split(item.lineBreak) : item.text);
}
function multiLineOffset(item) {
  const tl = textLines(item);
  return (isArray(tl) ? tl.length - 1 : 0) * lineHeight(item);
}
function textValue(item, line) {
  const text = line == null ? '' : (line + '').trim();
  return item.limit > 0 && text.length ? truncate(item, text) : text;
}
function widthGetter(item) {
  if (textMetrics.width === measureWidth) {
    // we are using canvas
    const currentFont = font$1(item);
    return text => _measureWidth(text, currentFont);
  } else if (textMetrics.width === estimateWidth) {
    // we are relying on estimates
    const currentFontHeight = fontSize(item);
    return text => _estimateWidth(text, currentFontHeight);
  } else {
    // User defined textMetrics.width function in use (e.g. vl-convert)
    return text => textMetrics.width(item, text);
  }
}
function truncate(item, text) {
  var limit = +item.limit,
    width = widthGetter(item);
  if (width(text) < limit) return text;
  var ellipsis = item.ellipsis || '\u2026',
    rtl = item.dir === 'rtl',
    lo = 0,
    hi = text.length,
    mid;
  limit -= width(ellipsis);
  if (rtl) {
    while (lo < hi) {
      mid = lo + hi >>> 1;
      if (width(text.slice(mid)) > limit) lo = mid + 1;else hi = mid;
    }
    return ellipsis + text.slice(lo);
  } else {
    while (lo < hi) {
      mid = 1 + (lo + hi >>> 1);
      if (width(text.slice(0, mid)) < limit) lo = mid;else hi = mid - 1;
    }
    return text.slice(0, lo) + ellipsis;
  }
}
function fontFamily(item, quote) {
  var font = item.font;
  return (quote && font ? String(font).replace(/"/g, '\'') : font) || 'sans-serif';
}
function font$1(item, quote) {
  return '' + (item.fontStyle ? item.fontStyle + ' ' : '') + (item.fontVariant ? item.fontVariant + ' ' : '') + (item.fontWeight ? item.fontWeight + ' ' : '') + fontSize(item) + 'px ' + fontFamily(item, quote);
}
function offset$2(item) {
  // perform our own font baseline calculation
  // why? not all browsers support SVG 1.1 'alignment-baseline' :(
  // this also ensures consistent layout across renderers
  var baseline = item.baseline,
    h = fontSize(item);
  return Math.round(baseline === 'top' ? 0.79 * h : baseline === 'middle' ? 0.30 * h : baseline === 'bottom' ? -0.21 * h : baseline === 'line-top' ? 0.29 * h + 0.5 * lineHeight(item) : baseline === 'line-bottom' ? 0.29 * h - 0.5 * lineHeight(item) : 0);
}

const textAlign = {
  'left': 'start',
  'center': 'middle',
  'right': 'end'
};
const tempBounds$1 = new Bounds();
function anchorPoint(item) {
  var x = item.x || 0,
    y = item.y || 0,
    r = item.radius || 0,
    t;
  if (r) {
    t = (item.theta || 0) - HalfPi;
    x += r * Math.cos(t);
    y += r * Math.sin(t);
  }
  tempBounds$1.x1 = x;
  tempBounds$1.y1 = y;
  return tempBounds$1;
}
function attr(emit, item) {
  var dx = item.dx || 0,
    dy = (item.dy || 0) + offset$2(item),
    p = anchorPoint(item),
    x = p.x1,
    y = p.y1,
    a = item.angle || 0,
    t;
  emit('text-anchor', textAlign[item.align] || 'start');
  if (a) {
    t = translate$2(x, y) + ' ' + rotate(a);
    if (dx || dy) t += ' ' + translate$2(dx, dy);
  } else {
    t = translate$2(x + dx, y + dy);
  }
  emit('transform', t);
}
function bound(bounds, item, mode) {
  var h = textMetrics.height(item),
    a = item.align,
    p = anchorPoint(item),
    x = p.x1,
    y = p.y1,
    dx = item.dx || 0,
    dy = (item.dy || 0) + offset$2(item) - Math.round(0.8 * h),
    // use 4/5 offset
    tl = textLines(item),
    w;

  // get dimensions
  if (isArray(tl)) {
    // multi-line text
    h += lineHeight(item) * (tl.length - 1);
    w = tl.reduce((w, t) => Math.max(w, textMetrics.width(item, t)), 0);
  } else {
    // single-line text
    w = textMetrics.width(item, tl);
  }

  // horizontal alignment
  if (a === 'center') {
    dx -= w / 2;
  } else if (a === 'right') {
    dx -= w;
  } else ;
  bounds.set(dx += x, dy += y, dx + w, dy + h);
  if (item.angle && !mode) {
    bounds.rotate(item.angle * DegToRad, x, y);
  } else if (mode === 2) {
    return bounds.rotatedPoints(item.angle * DegToRad, x, y);
  }
  return bounds;
}
function draw$5(context, scene, bounds) {
  visit(scene, item => {
    var opacity = item.opacity == null ? 1 : item.opacity,
      p,
      x,
      y,
      i,
      lh,
      tl,
      str;
    if (bounds && !bounds.intersects(item.bounds) ||
    // bounds check
    opacity === 0 || item.fontSize <= 0 || item.text == null || item.text.length === 0) return;
    context.font = font$1(item);
    context.textAlign = item.align || 'left';
    p = anchorPoint(item);
    x = p.x1, y = p.y1;
    if (item.angle) {
      context.save();
      context.translate(x, y);
      context.rotate(item.angle * DegToRad);
      x = y = 0; // reset x, y
    }
    x += item.dx || 0;
    y += (item.dy || 0) + offset$2(item);
    tl = textLines(item);
    blend(context, item);
    if (isArray(tl)) {
      lh = lineHeight(item);
      for (i = 0; i < tl.length; ++i) {
        str = textValue(item, tl[i]);
        if (item.fill && fill(context, item, opacity)) {
          context.fillText(str, x, y);
        }
        if (item.stroke && stroke(context, item, opacity)) {
          context.strokeText(str, x, y);
        }
        y += lh;
      }
    } else {
      str = textValue(item, tl);
      if (item.fill && fill(context, item, opacity)) {
        context.fillText(str, x, y);
      }
      if (item.stroke && stroke(context, item, opacity)) {
        context.strokeText(str, x, y);
      }
    }
    if (item.angle) context.restore();
  });
}
function hit(context, item, x, y, gx, gy) {
  if (item.fontSize <= 0) return false;
  if (!item.angle) return true; // bounds sufficient if no rotation

  // project point into space of unrotated bounds
  var p = anchorPoint(item),
    ax = p.x1,
    ay = p.y1,
    b = bound(tempBounds$1, item, 1),
    a = -item.angle * DegToRad,
    cos = Math.cos(a),
    sin = Math.sin(a),
    px = cos * gx - sin * gy + (ax - cos * ax + sin * ay),
    py = sin * gx + cos * gy + (ay - sin * ax - cos * ay);
  return b.contains(px, py);
}
function intersectText(item, box) {
  const p = bound(tempBounds$1, item, 2);
  return intersectBoxLine(box, p[0], p[1], p[2], p[3]) || intersectBoxLine(box, p[0], p[1], p[4], p[5]) || intersectBoxLine(box, p[4], p[5], p[6], p[7]) || intersectBoxLine(box, p[2], p[3], p[6], p[7]);
}
var text$2 = {
  type: 'text',
  tag: 'text',
  nested: false,
  attr: attr,
  bound: bound,
  draw: draw$5,
  pick: pick$1(hit),
  isect: intersectText
};

var trail$2 = markMultiItemPath('trail', trail$1, pickTrail);

var Marks = {
  arc: arc$2,
  area: area$2,
  group: group,
  image: image$1,
  line: line$2,
  path: path$2,
  rect: rect$1,
  rule: rule$2,
  shape: shape,
  symbol: symbol,
  text: text$2,
  trail: trail$2
};

function boundItem$1 (item, func, opt) {
  var type = Marks[item.mark.marktype],
    bound = func || type.bound;
  if (type.nested) item = item.mark;
  return bound(item.bounds || (item.bounds = new Bounds()), item, opt);
}

var DUMMY = {
  mark: null
};
function boundMark (mark, bounds, opt) {
  var type = Marks[mark.marktype],
    bound = type.bound,
    items = mark.items,
    hasItems = items && items.length,
    i,
    n,
    item,
    b;
  if (type.nested) {
    if (hasItems) {
      item = items[0];
    } else {
      // no items, fake it
      DUMMY.mark = mark;
      item = DUMMY;
    }
    b = boundItem$1(item, bound, opt);
    bounds = bounds && bounds.union(b) || b;
    return bounds;
  }
  bounds = bounds || mark.bounds && mark.bounds.clear() || new Bounds();
  if (hasItems) {
    for (i = 0, n = items.length; i < n; ++i) {
      bounds.union(boundItem$1(items[i], bound, opt));
    }
  }
  return mark.bounds = bounds;
}

const keys$2 = ['marktype', 'name', 'role', 'interactive', 'clip', 'items', 'zindex', 'x', 'y', 'width', 'height', 'align', 'baseline',
// layout
'fill', 'fillOpacity', 'opacity', 'blend',
// fill
'stroke', 'strokeOpacity', 'strokeWidth', 'strokeCap',
// stroke
'strokeDash', 'strokeDashOffset',
// stroke dash
'strokeForeground', 'strokeOffset',
// group
'startAngle', 'endAngle', 'innerRadius', 'outerRadius',
// arc
'cornerRadius', 'padAngle',
// arc, rect
'cornerRadiusTopLeft', 'cornerRadiusTopRight',
// rect, group
'cornerRadiusBottomLeft', 'cornerRadiusBottomRight', 'interpolate', 'tension', 'orient', 'defined',
// area, line
'url', 'aspect', 'smooth',
// image
'path', 'scaleX', 'scaleY',
// path
'x2', 'y2',
// rule
'size', 'shape',
// symbol
'text', 'angle', 'theta', 'radius', 'dir', 'dx', 'dy',
// text
'ellipsis', 'limit', 'lineBreak', 'lineHeight', 'font', 'fontSize', 'fontWeight', 'fontStyle', 'fontVariant',
// font
'description', 'aria', 'ariaRole', 'ariaRoleDescription' // aria
];
function sceneToJSON(scene, indent) {
  return JSON.stringify(scene, keys$2, indent);
}
function sceneFromJSON(json) {
  const scene = typeof json === 'string' ? JSON.parse(json) : json;
  return initialize$1(scene);
}
function initialize$1(scene) {
  var type = scene.marktype,
    items = scene.items,
    parent,
    i,
    n;
  if (items) {
    for (i = 0, n = items.length; i < n; ++i) {
      parent = type ? 'mark' : 'group';
      items[i][parent] = scene;
      if (items[i].zindex) items[i][parent].zdirty = true;
      if ('group' === (type || parent)) initialize$1(items[i]);
    }
  }
  if (type) boundMark(scene);
  return scene;
}

class Scenegraph {
  constructor(scene) {
    if (arguments.length) {
      this.root = sceneFromJSON(scene);
    } else {
      this.root = createMark({
        marktype: 'group',
        name: 'root',
        role: 'frame'
      });
      this.root.items = [new GroupItem(this.root)];
    }
  }
  toJSON(indent) {
    return sceneToJSON(this.root, indent || 0);
  }
  mark(markdef, group, index) {
    group = group || this.root.items[0];
    const mark = createMark(markdef, group);
    group.items[index] = mark;
    if (mark.zindex) mark.group.zdirty = true;
    return mark;
  }
}
function createMark(def, group) {
  const mark = {
    bounds: new Bounds(),
    clip: !!def.clip,
    group: group,
    interactive: def.interactive === false ? false : true,
    items: [],
    marktype: def.marktype,
    name: def.name || undefined,
    role: def.role || undefined,
    zindex: def.zindex || 0
  };

  // add accessibility properties if defined
  if (def.aria != null) {
    mark.aria = def.aria;
  }
  if (def.description) {
    mark.description = def.description;
  }
  return mark;
}

// create a new DOM element
function domCreate(doc, tag, ns) {
  if (!doc && typeof document !== 'undefined' && document.createElement) {
    doc = document;
  }
  return doc ? ns ? doc.createElementNS(ns, tag) : doc.createElement(tag) : null;
}

// find first child element with matching tag
function domFind(el, tag) {
  tag = tag.toLowerCase();
  var nodes = el.childNodes,
    i = 0,
    n = nodes.length;
  for (; i < n; ++i) if (nodes[i].tagName.toLowerCase() === tag) {
    return nodes[i];
  }
}

// retrieve child element at given index
// create & insert if doesn't exist or if tags do not match
function domChild(el, index, tag, ns) {
  var a = el.childNodes[index],
    b;
  if (!a || a.tagName.toLowerCase() !== tag.toLowerCase()) {
    b = a || null;
    a = domCreate(el.ownerDocument, tag, ns);
    el.insertBefore(a, b);
  }
  return a;
}

// remove all child elements at or above the given index
function domClear(el, index) {
  var nodes = el.childNodes,
    curr = nodes.length;
  while (curr > index) el.removeChild(nodes[--curr]);
  return el;
}

// generate css class name for mark
function cssClass(mark) {
  return 'mark-' + mark.marktype + (mark.role ? ' role-' + mark.role : '') + (mark.name ? ' ' + mark.name : '');
}

function point$2 (event, el) {
  const rect = el.getBoundingClientRect();
  return [event.clientX - rect.left - (el.clientLeft || 0), event.clientY - rect.top - (el.clientTop || 0)];
}

function resolveItem (item, event, el, origin) {
  var mark = item && item.mark,
    mdef,
    p;
  if (mark && (mdef = Marks[mark.marktype]).tip) {
    p = point$2(event, el);
    p[0] -= origin[0];
    p[1] -= origin[1];
    while (item = item.mark.group) {
      p[0] -= item.x || 0;
      p[1] -= item.y || 0;
    }
    item = mdef.tip(mark.items, p);
  }
  return item;
}

let Handler$1 = class Handler {
  /**
   * Create a new Handler instance.
   * @param {object} [customLoader] - Optional loader instance for
   *   href URL sanitization. If not specified, a standard loader
   *   instance will be generated.
   * @param {function} [customTooltip] - Optional tooltip handler
   *   function for custom tooltip display.
   * @constructor
   */
  constructor(customLoader, customTooltip) {
    this._active = null;
    this._handlers = {};
    this._loader = customLoader || loader();
    this._tooltip = customTooltip || defaultTooltip$1;
  }

  /**
   * Initialize a new Handler instance.
   * @param {DOMElement} el - The containing DOM element for the display.
   * @param {Array<number>} origin - The origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {object} [obj] - Optional context object that should serve as
   *   the "this" context for event callbacks.
   * @return {Handler} - This handler instance.
   */
  initialize(el, origin, obj) {
    this._el = el;
    this._obj = obj || null;
    return this.origin(origin);
  }

  /**
   * Returns the parent container element for a visualization.
   * @return {DOMElement} - The containing DOM element.
   */
  element() {
    return this._el;
  }

  /**
   * Returns the scene element (e.g., canvas or SVG) of the visualization
   * Subclasses must override if the first child is not the scene element.
   * @return {DOMElement} - The scene (e.g., canvas or SVG) element.
   */
  canvas() {
    return this._el && this._el.firstChild;
  }

  /**
   * Get / set the origin coordinates of the visualization.
   */
  origin(origin) {
    if (arguments.length) {
      this._origin = origin || [0, 0];
      return this;
    } else {
      return this._origin.slice();
    }
  }

  /**
   * Get / set the scenegraph root.
   */
  scene(scene) {
    if (!arguments.length) return this._scene;
    this._scene = scene;
    return this;
  }

  /**
   * Add an event handler. Subclasses should override this method.
   */
  on(/*type, handler*/) {}

  /**
   * Remove an event handler. Subclasses should override this method.
   */
  off(/*type, handler*/) {}

  /**
   * Utility method for finding the array index of an event handler.
   * @param {Array} h - An array of registered event handlers.
   * @param {string} type - The event type.
   * @param {function} handler - The event handler instance to find.
   * @return {number} - The handler's array index or -1 if not registered.
   */
  _handlerIndex(h, type, handler) {
    for (let i = h ? h.length : 0; --i >= 0;) {
      if (h[i].type === type && (!handler || h[i].handler === handler)) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Returns an array with registered event handlers.
   * @param {string} [type] - The event type to query. Any annotations
   *   are ignored; for example, for the argument "click.foo", ".foo" will
   *   be ignored and the method returns all "click" handlers. If type is
   *   null or unspecified, this method returns handlers for all types.
   * @return {Array} - A new array containing all registered event handlers.
   */
  handlers(type) {
    const h = this._handlers,
      a = [];
    if (type) {
      a.push(...h[this.eventName(type)]);
    } else {
      for (const k in h) {
        a.push(...h[k]);
      }
    }
    return a;
  }

  /**
   * Parses an event name string to return the specific event type.
   * For example, given "click.foo" returns "click"
   * @param {string} name - The input event type string.
   * @return {string} - A string with the event type only.
   */
  eventName(name) {
    const i = name.indexOf('.');
    return i < 0 ? name : name.slice(0, i);
  }

  /**
   * Handle hyperlink navigation in response to an item.href value.
   * @param {Event} event - The event triggering hyperlink navigation.
   * @param {Item} item - The scenegraph item.
   * @param {string} href - The URL to navigate to.
   */
  handleHref(event, item, href) {
    this._loader.sanitize(href, {
      context: 'href'
    }).then(opt => {
      const e = new MouseEvent(event.type, event),
        a = domCreate(null, 'a');
      for (const name in opt) a.setAttribute(name, opt[name]);
      a.dispatchEvent(e);
    }).catch(() => {});
  }

  /**
   * Handle tooltip display in response to an item.tooltip value.
   * @param {Event} event - The event triggering tooltip display.
   * @param {Item} item - The scenegraph item.
   * @param {boolean} show - A boolean flag indicating whether
   *   to show or hide a tooltip for the given item.
   */
  handleTooltip(event, item, show) {
    if (item && item.tooltip != null) {
      item = resolveItem(item, event, this.canvas(), this._origin);
      const value = show && item && item.tooltip || null;
      this._tooltip.call(this._obj, this, event, item, value);
    }
  }

  /**
   * Returns the size of a scenegraph item and its position relative
   * to the viewport.
   * @param {Item} item - The scenegraph item.
   * @return {object} - A bounding box object (compatible with the
   *   DOMRect type) consisting of x, y, width, heigh, top, left,
   *   right, and bottom properties.
   */
  getItemBoundingClientRect(item) {
    const el = this.canvas();
    if (!el) return;
    const rect = el.getBoundingClientRect(),
      origin = this._origin,
      bounds = item.bounds,
      width = bounds.width(),
      height = bounds.height();
    let x = bounds.x1 + origin[0] + rect.left,
      y = bounds.y1 + origin[1] + rect.top;

    // translate coordinate for each parent group
    while (item.mark && (item = item.mark.group)) {
      x += item.x || 0;
      y += item.y || 0;
    }

    // return DOMRect-compatible bounding box
    return {
      x,
      y,
      width,
      height,
      left: x,
      top: y,
      right: x + width,
      bottom: y + height
    };
  }
};

// The default tooltip display handler.
// Sets the HTML title attribute on the visualization container.
function defaultTooltip$1(handler, event, item, value) {
  handler.element().setAttribute('title', value || '');
}

class Renderer {
  /**
   * Create a new Renderer instance.
   * @param {object} [loader] - Optional loader instance for
   *   image and href URL sanitization. If not specified, a
   *   standard loader instance will be generated.
   * @constructor
   */
  constructor(loader) {
    this._el = null;
    this._bgcolor = null;
    this._loader = new ResourceLoader(loader);
  }

  /**
   * Initialize a new Renderer instance.
   * @param {DOMElement} el - The containing DOM element for the display.
   * @param {number} width - The coordinate width of the display, in pixels.
   * @param {number} height - The coordinate height of the display, in pixels.
   * @param {Array<number>} origin - The origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {number} [scaleFactor=1] - Optional scaleFactor by which to multiply
   *   the width and height to determine the final pixel size.
   * @return {Renderer} - This renderer instance.
   */
  initialize(el, width, height, origin, scaleFactor) {
    this._el = el;
    return this.resize(width, height, origin, scaleFactor);
  }

  /**
   * Returns the parent container element for a visualization.
   * @return {DOMElement} - The containing DOM element.
   */
  element() {
    return this._el;
  }

  /**
   * Returns the scene element (e.g., canvas or SVG) of the visualization
   * Subclasses must override if the first child is not the scene element.
   * @return {DOMElement} - The scene (e.g., canvas or SVG) element.
   */
  canvas() {
    return this._el && this._el.firstChild;
  }

  /**
   * Get / set the background color.
   */
  background(bgcolor) {
    if (arguments.length === 0) return this._bgcolor;
    this._bgcolor = bgcolor;
    return this;
  }

  /**
   * Resize the display.
   * @param {number} width - The new coordinate width of the display, in pixels.
   * @param {number} height - The new coordinate height of the display, in pixels.
   * @param {Array<number>} origin - The new origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {number} [scaleFactor=1] - Optional scaleFactor by which to multiply
   *   the width and height to determine the final pixel size.
   * @return {Renderer} - This renderer instance;
   */
  resize(width, height, origin, scaleFactor) {
    this._width = width;
    this._height = height;
    this._origin = origin || [0, 0];
    this._scale = scaleFactor || 1;
    return this;
  }

  /**
   * Report a dirty item whose bounds should be redrawn.
   * This base class method does nothing. Subclasses that perform
   * incremental should implement this method.
   * @param {Item} item - The dirty item whose bounds should be redrawn.
   */
  dirty(/*item*/) {}

  /**
   * Render an input scenegraph, potentially with a set of dirty items.
   * This method will perform an immediate rendering with available resources.
   * The renderer may also need to perform image loading to perform a complete
   * render. This process can lead to asynchronous re-rendering of the scene
   * after this method returns. To receive notification when rendering is
   * complete, use the renderAsync method instead.
   * @param {object} scene - The root mark of a scenegraph to render.
   * @param {Array} markTypes - Array of the mark types to render.
   *                            If undefined, render all mark types
   * @return {Renderer} - This renderer instance.
   */
  render(scene, markTypes) {
    const r = this;

    // bind arguments into a render call, and cache it
    // this function may be subsequently called for async redraw
    r._call = function () {
      r._render(scene, markTypes);
    };

    // invoke the renderer
    r._call();

    // clear the cached call for garbage collection
    // async redraws will stash their own copy
    r._call = null;
    return r;
  }

  /**
   * Internal rendering method. Renderer subclasses should override this
   * method to actually perform rendering.
   * @param {object} scene - The root mark of a scenegraph to render.
   * @param {Array} markTypes - Array of the mark types to render.
   *                            If undefined, render all mark types
   */
  _render(/*scene, markTypes*/
  ) {
    // subclasses to override
  }

  /**
   * Asynchronous rendering method. Similar to render, but returns a Promise
   * that resolves when all rendering is completed. Sometimes a renderer must
   * perform image loading to get a complete rendering. The returned
   * Promise will not resolve until this process completes.
   * @param {object} scene - The root mark of a scenegraph to render.
   * @param {Array} markTypes - Array of the mark types to render.
   *                            If undefined, render all mark types
   * @return {Promise} - A Promise that resolves when rendering is complete.
   */
  renderAsync(scene, markTypes) {
    const r = this.render(scene, markTypes);
    return this._ready ? this._ready.then(() => r) : Promise.resolve(r);
  }

  /**
   * Internal method for asynchronous resource loading.
   * Proxies method calls to the ImageLoader, and tracks loading
   * progress to invoke a re-render once complete.
   * @param {string} method - The method name to invoke on the ImageLoader.
   * @param {string} uri - The URI for the requested resource.
   * @return {Promise} - A Promise that resolves to the requested resource.
   */
  _load(method, uri) {
    var r = this,
      p = r._loader[method](uri);
    if (!r._ready) {
      // re-render the scene when loading completes
      const call = r._call;
      r._ready = r._loader.ready().then(redraw => {
        if (redraw) call();
        r._ready = null;
      });
    }
    return p;
  }

  /**
   * Sanitize a URL to include as a hyperlink in the rendered scene.
   * This method proxies a call to ImageLoader.sanitizeURL, but also tracks
   * image loading progress and invokes a re-render once complete.
   * @param {string} uri - The URI string to sanitize.
   * @return {Promise} - A Promise that resolves to the sanitized URL.
   */
  sanitizeURL(uri) {
    return this._load('sanitizeURL', uri);
  }

  /**
   * Requests an image to include in the rendered scene.
   * This method proxies a call to ImageLoader.loadImage, but also tracks
   * image loading progress and invokes a re-render once complete.
   * @param {string} uri - The URI string of the image.
   * @return {Promise} - A Promise that resolves to the loaded Image.
   */
  loadImage(uri) {
    return this._load('loadImage', uri);
  }
}

const KeyDownEvent = 'keydown';
const KeyPressEvent = 'keypress';
const KeyUpEvent = 'keyup';
const DragEnterEvent = 'dragenter';
const DragLeaveEvent = 'dragleave';
const DragOverEvent = 'dragover';
const PointerDownEvent = 'pointerdown';
const PointerUpEvent = 'pointerup';
const PointerMoveEvent = 'pointermove';
const PointerOutEvent = 'pointerout';
const PointerOverEvent = 'pointerover';
const MouseDownEvent = 'mousedown';
const MouseUpEvent = 'mouseup';
const MouseMoveEvent = 'mousemove';
const MouseOutEvent = 'mouseout';
const MouseOverEvent = 'mouseover';
const ClickEvent = 'click';
const DoubleClickEvent = 'dblclick';
const WheelEvent = 'wheel';
const MouseWheelEvent = 'mousewheel';
const TouchStartEvent = 'touchstart';
const TouchMoveEvent = 'touchmove';
const TouchEndEvent = 'touchend';
const Events = [KeyDownEvent, KeyPressEvent, KeyUpEvent, DragEnterEvent, DragLeaveEvent, DragOverEvent, PointerDownEvent, PointerUpEvent, PointerMoveEvent, PointerOutEvent, PointerOverEvent, MouseDownEvent, MouseUpEvent, MouseMoveEvent, MouseOutEvent, MouseOverEvent, ClickEvent, DoubleClickEvent, WheelEvent, MouseWheelEvent, TouchStartEvent, TouchMoveEvent, TouchEndEvent];
const TooltipShowEvent = PointerMoveEvent;
const TooltipHideEvent = MouseOutEvent;
const HrefEvent = ClickEvent;

class CanvasHandler extends Handler$1 {
  constructor(loader, tooltip) {
    super(loader, tooltip);
    this._down = null;
    this._touch = null;
    this._first = true;
    this._events = {};

    // supported events
    this.events = Events;
    this.pointermove = move([PointerMoveEvent, MouseMoveEvent], [PointerOverEvent, MouseOverEvent], [PointerOutEvent, MouseOutEvent]);
    this.dragover = move([DragOverEvent], [DragEnterEvent], [DragLeaveEvent]), this.pointerout = inactive([PointerOutEvent, MouseOutEvent]);
    this.dragleave = inactive([DragLeaveEvent]);
  }
  initialize(el, origin, obj) {
    this._canvas = el && domFind(el, 'canvas');

    // add minimal events required for proper state management
    [ClickEvent, MouseDownEvent, PointerDownEvent, PointerMoveEvent, PointerOutEvent, DragLeaveEvent].forEach(type => eventListenerCheck(this, type));
    return super.initialize(el, origin, obj);
  }

  // return the backing canvas instance
  canvas() {
    return this._canvas;
  }

  // retrieve the current canvas context
  context() {
    return this._canvas.getContext('2d');
  }

  // to keep old versions of firefox happy
  DOMMouseScroll(evt) {
    this.fire(MouseWheelEvent, evt);
  }
  pointerdown(evt) {
    this._down = this._active;
    this.fire(PointerDownEvent, evt);
  }
  mousedown(evt) {
    this._down = this._active;
    this.fire(MouseDownEvent, evt);
  }
  click(evt) {
    if (this._down === this._active) {
      this.fire(ClickEvent, evt);
      this._down = null;
    }
  }
  touchstart(evt) {
    this._touch = this.pickEvent(evt.changedTouches[0]);
    if (this._first) {
      this._active = this._touch;
      this._first = false;
    }
    this.fire(TouchStartEvent, evt, true);
  }
  touchmove(evt) {
    this.fire(TouchMoveEvent, evt, true);
  }
  touchend(evt) {
    this.fire(TouchEndEvent, evt, true);
    this._touch = null;
  }

  // fire an event
  fire(type, evt, touch) {
    const a = touch ? this._touch : this._active,
      h = this._handlers[type];

    // set event type relative to scenegraph items
    evt.vegaType = type;

    // handle hyperlinks and tooltips first
    if (type === HrefEvent && a && a.href) {
      this.handleHref(evt, a, a.href);
    } else if (type === TooltipShowEvent || type === TooltipHideEvent) {
      this.handleTooltip(evt, a, type !== TooltipHideEvent);
    }

    // invoke all registered handlers
    if (h) {
      for (let i = 0, len = h.length; i < len; ++i) {
        h[i].handler.call(this._obj, evt, a);
      }
    }
  }

  // add an event handler
  on(type, handler) {
    const name = this.eventName(type),
      h = this._handlers,
      i = this._handlerIndex(h[name], type, handler);
    if (i < 0) {
      eventListenerCheck(this, type);
      (h[name] || (h[name] = [])).push({
        type: type,
        handler: handler
      });
    }
    return this;
  }

  // remove an event handler
  off(type, handler) {
    const name = this.eventName(type),
      h = this._handlers[name],
      i = this._handlerIndex(h, type, handler);
    if (i >= 0) {
      h.splice(i, 1);
    }
    return this;
  }
  pickEvent(evt) {
    const p = point$2(evt, this._canvas),
      o = this._origin;
    return this.pick(this._scene, p[0], p[1], p[0] - o[0], p[1] - o[1]);
  }

  // find the scenegraph item at the current pointer position
  // x, y -- the absolute x, y pointer coordinates on the canvas element
  // gx, gy -- the relative coordinates within the current group
  pick(scene, x, y, gx, gy) {
    const g = this.context(),
      mark = Marks[scene.marktype];
    return mark.pick.call(this, g, scene, x, y, gx, gy);
  }
}
const eventBundle = type => type === TouchStartEvent || type === TouchMoveEvent || type === TouchEndEvent ? [TouchStartEvent, TouchMoveEvent, TouchEndEvent] : [type];

// lazily add listeners to the canvas as needed
function eventListenerCheck(handler, type) {
  eventBundle(type).forEach(_ => addEventListener(handler, _));
}
function addEventListener(handler, type) {
  const canvas = handler.canvas();
  if (canvas && !handler._events[type]) {
    handler._events[type] = 1;
    canvas.addEventListener(type, handler[type] ? evt => handler[type](evt) : evt => handler.fire(type, evt));
  }
}
function fireAll(handler, types, event) {
  types.forEach(type => handler.fire(type, event));
}
function move(moveEvents, overEvents, outEvents) {
  return function (evt) {
    const a = this._active,
      p = this.pickEvent(evt);
    if (p === a) {
      // active item and picked item are the same
      fireAll(this, moveEvents, evt); // fire move
    } else {
      // active item and picked item are different
      if (!a || !a.exit) {
        // fire out for prior active item
        // suppress if active item was removed from scene
        fireAll(this, outEvents, evt);
      }
      this._active = p; // set new active item
      fireAll(this, overEvents, evt); // fire over for new active item
      fireAll(this, moveEvents, evt); // fire move for new active item
    }
  };
}
function inactive(types) {
  return function (evt) {
    fireAll(this, types, evt);
    this._active = null;
  };
}

function devicePixelRatio() {
  return typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
}
function resize (canvas, width, height, origin, scaleFactor, opt) {
  const inDOM = typeof HTMLElement !== 'undefined' && canvas instanceof HTMLElement && canvas.parentNode != null,
    context = canvas.getContext('2d'),
    ratio = inDOM ? devicePixelRatio() : scaleFactor;
  canvas.width = width * ratio;
  canvas.height = height * ratio;
  for (const key in opt) {
    context[key] = opt[key];
  }
  if (inDOM && ratio !== 1) {
    canvas.style.width = width + 'px';
    canvas.style.height = height + 'px';
  }
  context.pixelRatio = ratio;
  context.setTransform(ratio, 0, 0, ratio, ratio * origin[0], ratio * origin[1]);
  return canvas;
}

class CanvasRenderer extends Renderer {
  constructor(loader) {
    super(loader);
    this._options = {};
    this._redraw = false;
    this._dirty = new Bounds();
    this._tempb = new Bounds();
  }
  initialize(el, width, height, origin, scaleFactor, options) {
    this._options = options || {};
    this._canvas = this._options.externalContext ? null : domCanvas(1, 1, this._options.type); // instantiate a small canvas

    if (el && this._canvas) {
      domClear(el, 0).appendChild(this._canvas);
      this._canvas.setAttribute('class', 'marks');
    }

    // this method will invoke resize to size the canvas appropriately
    return super.initialize(el, width, height, origin, scaleFactor);
  }
  resize(width, height, origin, scaleFactor) {
    super.resize(width, height, origin, scaleFactor);
    if (this._canvas) {
      // configure canvas size and transform
      resize(this._canvas, this._width, this._height, this._origin, this._scale, this._options.context);
    } else {
      // external context needs to be scaled and positioned to origin
      const ctx = this._options.externalContext;
      if (!ctx) error$1('CanvasRenderer is missing a valid canvas or context');
      ctx.scale(this._scale, this._scale);
      ctx.translate(this._origin[0], this._origin[1]);
    }
    this._redraw = true;
    return this;
  }
  canvas() {
    return this._canvas;
  }
  context() {
    return this._options.externalContext || (this._canvas ? this._canvas.getContext('2d') : null);
  }
  dirty(item) {
    const b = this._tempb.clear().union(item.bounds);
    let g = item.mark.group;
    while (g) {
      b.translate(g.x || 0, g.y || 0);
      g = g.mark.group;
    }
    this._dirty.union(b);
  }
  _render(scene, markTypes) {
    const g = this.context(),
      o = this._origin,
      w = this._width,
      h = this._height,
      db = this._dirty,
      vb = viewBounds(o, w, h);

    // setup
    g.save();
    const b = this._redraw || db.empty() ? (this._redraw = false, vb.expand(1)) : clipToBounds(g, vb.intersect(db), o);
    this.clear(-o[0], -o[1], w, h);

    // render
    this.draw(g, scene, b, markTypes);

    // takedown
    g.restore();
    db.clear();
    return this;
  }
  draw(ctx, scene, bounds, markTypes) {
    if (scene.marktype !== 'group' && markTypes != null && !markTypes.includes(scene.marktype)) {
      return;
    }
    const mark = Marks[scene.marktype];
    if (scene.clip) clip$2(ctx, scene);
    mark.draw.call(this, ctx, scene, bounds, markTypes);
    if (scene.clip) ctx.restore();
  }
  clear(x, y, w, h) {
    const opt = this._options,
      g = this.context();
    if (opt.type !== 'pdf' && !opt.externalContext) {
      // calling clear rect voids vector output in pdf mode
      // and could remove external context content (#2615)
      g.clearRect(x, y, w, h);
    }
    if (this._bgcolor != null) {
      g.fillStyle = this._bgcolor;
      g.fillRect(x, y, w, h);
    }
  }
}
const viewBounds = (origin, width, height) => new Bounds().set(0, 0, width, height).translate(-origin[0], -origin[1]);
function clipToBounds(g, b, origin) {
  // expand bounds by 1 pixel, then round to pixel boundaries
  b.expand(1).round();

  // align to base pixel grid in case of non-integer scaling (#2425)
  if (g.pixelRatio % 1) {
    b.scale(g.pixelRatio).round().scale(1 / g.pixelRatio);
  }

  // to avoid artifacts translate if origin has fractional pixels
  b.translate(-(origin[0] % 1), -(origin[1] % 1));

  // set clip path
  g.beginPath();
  g.rect(b.x1, b.y1, b.width(), b.height());
  g.clip();
  return b;
}

class SVGHandler extends Handler$1 {
  constructor(loader, tooltip) {
    super(loader, tooltip);
    const h = this;
    h._hrefHandler = listener(h, (evt, item) => {
      if (item && item.href) h.handleHref(evt, item, item.href);
    });
    h._tooltipHandler = listener(h, (evt, item) => {
      h.handleTooltip(evt, item, evt.type !== TooltipHideEvent);
    });
  }
  initialize(el, origin, obj) {
    let svg = this._svg;
    if (svg) {
      svg.removeEventListener(HrefEvent, this._hrefHandler);
      svg.removeEventListener(TooltipShowEvent, this._tooltipHandler);
      svg.removeEventListener(TooltipHideEvent, this._tooltipHandler);
    }
    this._svg = svg = el && domFind(el, 'svg');
    if (svg) {
      svg.addEventListener(HrefEvent, this._hrefHandler);
      svg.addEventListener(TooltipShowEvent, this._tooltipHandler);
      svg.addEventListener(TooltipHideEvent, this._tooltipHandler);
    }
    return super.initialize(el, origin, obj);
  }
  canvas() {
    return this._svg;
  }

  // add an event handler
  on(type, handler) {
    const name = this.eventName(type),
      h = this._handlers,
      i = this._handlerIndex(h[name], type, handler);
    if (i < 0) {
      const x = {
        type,
        handler,
        listener: listener(this, handler)
      };
      (h[name] || (h[name] = [])).push(x);
      if (this._svg) {
        this._svg.addEventListener(name, x.listener);
      }
    }
    return this;
  }

  // remove an event handler
  off(type, handler) {
    const name = this.eventName(type),
      h = this._handlers[name],
      i = this._handlerIndex(h, type, handler);
    if (i >= 0) {
      if (this._svg) {
        this._svg.removeEventListener(name, h[i].listener);
      }
      h.splice(i, 1);
    }
    return this;
  }
}

// wrap an event listener for the SVG DOM
const listener = (context, handler) => evt => {
  let item = evt.target.__data__;
  item = Array.isArray(item) ? item[0] : item;
  evt.vegaType = evt.type;
  handler.call(context._obj, evt, item);
};

const ARIA_HIDDEN = 'aria-hidden';
const ARIA_LABEL = 'aria-label';
const ARIA_ROLE = 'role';
const ARIA_ROLEDESCRIPTION = 'aria-roledescription';
const GRAPHICS_OBJECT = 'graphics-object';
const GRAPHICS_SYMBOL = 'graphics-symbol';
const bundle = (role, roledesc, label) => ({
  [ARIA_ROLE]: role,
  [ARIA_ROLEDESCRIPTION]: roledesc,
  [ARIA_LABEL]: label || undefined
});

// these roles are covered by related roles
// we can ignore them, no need to generate attributes
const AriaIgnore = toSet(['axis-domain', 'axis-grid', 'axis-label', 'axis-tick', 'axis-title', 'legend-band', 'legend-entry', 'legend-gradient', 'legend-label', 'legend-title', 'legend-symbol', 'title']);

// aria attribute generators for guide roles
const AriaGuides = {
  'axis': {
    desc: 'axis',
    caption: axisCaption
  },
  'legend': {
    desc: 'legend',
    caption: legendCaption
  },
  'title-text': {
    desc: 'title',
    caption: item => `Title text '${titleCaption(item)}'`
  },
  'title-subtitle': {
    desc: 'subtitle',
    caption: item => `Subtitle text '${titleCaption(item)}'`
  }
};

// aria properties generated for mark item encoding channels
const AriaEncode = {
  ariaRole: ARIA_ROLE,
  ariaRoleDescription: ARIA_ROLEDESCRIPTION,
  description: ARIA_LABEL
};
function ariaItemAttributes(emit, item) {
  const hide = item.aria === false;
  emit(ARIA_HIDDEN, hide || undefined);
  if (hide || item.description == null) {
    for (const prop in AriaEncode) {
      emit(AriaEncode[prop], undefined);
    }
  } else {
    const type = item.mark.marktype;
    emit(ARIA_LABEL, item.description);
    emit(ARIA_ROLE, item.ariaRole || (type === 'group' ? GRAPHICS_OBJECT : GRAPHICS_SYMBOL));
    emit(ARIA_ROLEDESCRIPTION, item.ariaRoleDescription || `${type} mark`);
  }
}
function ariaMarkAttributes(mark) {
  return mark.aria === false ? {
    [ARIA_HIDDEN]: true
  } : AriaIgnore[mark.role] ? null : AriaGuides[mark.role] ? ariaGuide(mark, AriaGuides[mark.role]) : ariaMark(mark);
}
function ariaMark(mark) {
  const type = mark.marktype;
  const recurse = type === 'group' || type === 'text' || mark.items.some(_ => _.description != null && _.aria !== false);
  return bundle(recurse ? GRAPHICS_OBJECT : GRAPHICS_SYMBOL, `${type} mark container`, mark.description);
}
function ariaGuide(mark, opt) {
  try {
    const item = mark.items[0],
      caption = opt.caption || (() => '');
    return bundle(opt.role || GRAPHICS_SYMBOL, opt.desc, item.description || caption(item));
  } catch (err) {
    return null;
  }
}
function titleCaption(item) {
  return array$2(item.text).join(' ');
}
function axisCaption(item) {
  const datum = item.datum,
    orient = item.orient,
    title = datum.title ? extractTitle(item) : null,
    ctx = item.context,
    scale = ctx.scales[datum.scale].value,
    locale = ctx.dataflow.locale(),
    type = scale.type,
    xy = orient === 'left' || orient === 'right' ? 'Y' : 'X';
  return `${xy}-axis` + (title ? ` titled '${title}'` : '') + ` for a ${isDiscrete$2(type) ? 'discrete' : type} scale` + ` with ${domainCaption(locale, scale, item)}`;
}
function legendCaption(item) {
  const datum = item.datum,
    title = datum.title ? extractTitle(item) : null,
    type = `${datum.type || ''} legend`.trim(),
    scales = datum.scales,
    props = Object.keys(scales),
    ctx = item.context,
    scale = ctx.scales[scales[props[0]]].value,
    locale = ctx.dataflow.locale();
  return capitalize(type) + (title ? ` titled '${title}'` : '') + ` for ${channelCaption(props)}` + ` with ${domainCaption(locale, scale, item)}`;
}
function extractTitle(item) {
  try {
    return array$2(peek$1(item.items).items[0].text).join(' ');
  } catch (err) {
    return null;
  }
}
function channelCaption(props) {
  props = props.map(p => p + (p === 'fill' || p === 'stroke' ? ' color' : ''));
  return props.length < 2 ? props[0] : props.slice(0, -1).join(', ') + ' and ' + peek$1(props);
}
function capitalize(s) {
  return s.length ? s[0].toUpperCase() + s.slice(1) : s;
}

const innerText = val => (val + '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const attrText = val => innerText(val).replace(/"/g, '&quot;').replace(/\t/g, '&#x9;').replace(/\n/g, '&#xA;').replace(/\r/g, '&#xD;');
function markup() {
  let buf = '',
    outer = '',
    inner = '';
  const stack = [],
    clear = () => outer = inner = '',
    push = tag => {
      if (outer) {
        buf += `${outer}>${inner}`;
        clear();
      }
      stack.push(tag);
    },
    attr = (name, value) => {
      if (value != null) outer += ` ${name}="${attrText(value)}"`;
      return m;
    },
    m = {
      open(tag, ...attrs) {
        push(tag);
        outer = '<' + tag;
        for (const set of attrs) {
          for (const key in set) attr(key, set[key]);
        }
        return m;
      },
      close() {
        const tag = stack.pop();
        if (outer) {
          buf += outer + (inner ? `>${inner}</${tag}>` : '/>');
        } else {
          buf += `</${tag}>`;
        }
        clear();
        return m;
      },
      attr,
      text: t => (inner += innerText(t), m),
      toString: () => buf
    };
  return m;
}
const serializeXML = node => _serialize(markup(), node) + '';
function _serialize(m, node) {
  m.open(node.tagName);
  if (node.hasAttributes()) {
    const attrs = node.attributes,
      n = attrs.length;
    for (let i = 0; i < n; ++i) {
      m.attr(attrs[i].name, attrs[i].value);
    }
  }
  if (node.hasChildNodes()) {
    const children = node.childNodes;
    for (const child of children) {
      child.nodeType === 3 // text node
      ? m.text(child.nodeValue) : _serialize(m, child);
    }
  }
  return m.close();
}

const stylesAttr = {
  fill: 'fill',
  fillOpacity: 'fill-opacity',
  stroke: 'stroke',
  strokeOpacity: 'stroke-opacity',
  strokeWidth: 'stroke-width',
  strokeCap: 'stroke-linecap',
  strokeJoin: 'stroke-linejoin',
  strokeDash: 'stroke-dasharray',
  strokeDashOffset: 'stroke-dashoffset',
  strokeMiterLimit: 'stroke-miterlimit',
  opacity: 'opacity'
};
const stylesCss = {
  blend: 'mix-blend-mode'
};

// ensure miter limit default is consistent with canvas (#2498)
const rootAttributes = {
  'fill': 'none',
  'stroke-miterlimit': 10
};

const RootIndex = 0,
  xmlns = 'http://www.w3.org/2000/xmlns/',
  svgns = metadata.xmlns;
class SVGRenderer extends Renderer {
  constructor(loader) {
    super(loader);
    this._dirtyID = 0;
    this._dirty = [];
    this._svg = null;
    this._root = null;
    this._defs = null;
  }

  /**
   * Initialize a new SVGRenderer instance.
   * @param {DOMElement} el - The containing DOM element for the display.
   * @param {number} width - The coordinate width of the display, in pixels.
   * @param {number} height - The coordinate height of the display, in pixels.
   * @param {Array<number>} origin - The origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {number} [scaleFactor=1] - Optional scaleFactor by which to multiply
   *   the width and height to determine the final pixel size.
   * @return {SVGRenderer} - This renderer instance.
   */
  initialize(el, width, height, origin, scaleFactor) {
    // create the svg definitions cache
    this._defs = {};
    this._clearDefs();
    if (el) {
      this._svg = domChild(el, 0, 'svg', svgns);
      this._svg.setAttributeNS(xmlns, 'xmlns', svgns);
      this._svg.setAttributeNS(xmlns, 'xmlns:xlink', metadata['xmlns:xlink']);
      this._svg.setAttribute('version', metadata['version']);
      this._svg.setAttribute('class', 'marks');
      domClear(el, 1);

      // set the svg root group
      this._root = domChild(this._svg, RootIndex, 'g', svgns);
      setAttributes(this._root, rootAttributes);

      // ensure no additional child elements
      domClear(this._svg, RootIndex + 1);
    }

    // set background color if defined
    this.background(this._bgcolor);
    return super.initialize(el, width, height, origin, scaleFactor);
  }

  /**
   * Get / set the background color.
   */
  background(bgcolor) {
    if (arguments.length && this._svg) {
      this._svg.style.setProperty('background-color', bgcolor);
    }
    return super.background(...arguments);
  }

  /**
   * Resize the display.
   * @param {number} width - The new coordinate width of the display, in pixels.
   * @param {number} height - The new coordinate height of the display, in pixels.
   * @param {Array<number>} origin - The new origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {number} [scaleFactor=1] - Optional scaleFactor by which to multiply
   *   the width and height to determine the final pixel size.
   * @return {SVGRenderer} - This renderer instance;
   */
  resize(width, height, origin, scaleFactor) {
    super.resize(width, height, origin, scaleFactor);
    if (this._svg) {
      setAttributes(this._svg, {
        width: this._width * this._scale,
        height: this._height * this._scale,
        viewBox: `0 0 ${this._width} ${this._height}`
      });
      this._root.setAttribute('transform', `translate(${this._origin})`);
    }
    this._dirty = [];
    return this;
  }

  /**
   * Returns the SVG element of the visualization.
   * @return {DOMElement} - The SVG element.
   */
  canvas() {
    return this._svg;
  }

  /**
   * Returns an SVG text string for the rendered content,
   * or null if this renderer is currently headless.
   */
  svg() {
    const svg = this._svg,
      bg = this._bgcolor;
    if (!svg) return null;
    let node;
    if (bg) {
      svg.removeAttribute('style');
      node = domChild(svg, RootIndex, 'rect', svgns);
      setAttributes(node, {
        width: this._width,
        height: this._height,
        fill: bg
      });
    }
    const text = serializeXML(svg);
    if (bg) {
      svg.removeChild(node);
      this._svg.style.setProperty('background-color', bg);
    }
    return text;
  }

  /**
   * Internal rendering method.
   * @param {object} scene - The root mark of a scenegraph to render.
   * @param {Array} markTypes - Array of the mark types to render.
   *                            If undefined, render all mark types
   */
  _render(scene, markTypes) {
    // perform spot updates and re-render markup
    if (this._dirtyCheck()) {
      if (this._dirtyAll) this._clearDefs();
      this.mark(this._root, scene, undefined, markTypes);
      domClear(this._root, 1);
    }
    this.defs();
    this._dirty = [];
    ++this._dirtyID;
    return this;
  }

  // -- Manage rendering of items marked as dirty --

  /**
   * Flag a mark item as dirty.
   * @param {Item} item - The mark item.
   */
  dirty(item) {
    if (item.dirty !== this._dirtyID) {
      item.dirty = this._dirtyID;
      this._dirty.push(item);
    }
  }

  /**
   * Check if a mark item is considered dirty.
   * @param {Item} item - The mark item.
   */
  isDirty(item) {
    return this._dirtyAll || !item._svg || !item._svg.ownerSVGElement || item.dirty === this._dirtyID;
  }

  /**
   * Internal method to check dirty status and, if possible,
   * make targetted updates without a full rendering pass.
   */
  _dirtyCheck() {
    this._dirtyAll = true;
    const items = this._dirty;
    if (!items.length || !this._dirtyID) return true;
    const id = ++this._dirtyID;
    let item, mark, type, mdef, i, n, o;
    for (i = 0, n = items.length; i < n; ++i) {
      item = items[i];
      mark = item.mark;
      if (mark.marktype !== type) {
        // memoize mark instance lookup
        type = mark.marktype;
        mdef = Marks[type];
      }
      if (mark.zdirty && mark.dirty !== id) {
        this._dirtyAll = false;
        dirtyParents(item, id);
        mark.items.forEach(i => {
          i.dirty = id;
        });
      }
      if (mark.zdirty) continue; // handle in standard drawing pass

      if (item.exit) {
        // EXIT
        if (mdef.nested && mark.items.length) {
          // if nested mark with remaining points, update instead
          o = mark.items[0];
          if (o._svg) this._update(mdef, o._svg, o);
        } else if (item._svg) {
          // otherwise remove from DOM
          o = item._svg.parentNode;
          if (o) o.removeChild(item._svg);
        }
        item._svg = null;
        continue;
      }
      item = mdef.nested ? mark.items[0] : item;
      if (item._update === id) continue; // already visited

      if (!item._svg || !item._svg.ownerSVGElement) {
        // ENTER
        this._dirtyAll = false;
        dirtyParents(item, id);
      } else {
        // IN-PLACE UPDATE
        this._update(mdef, item._svg, item);
      }
      item._update = id;
    }
    return !this._dirtyAll;
  }

  // -- Construct & maintain scenegraph to SVG mapping ---

  /**
   * Render a set of mark items.
   * @param {SVGElement} el - The parent element in the SVG tree.
   * @param {object} scene - The mark parent to render.
   * @param {SVGElement} prev - The previous sibling in the SVG tree.
   * @param {Array} markTypes - Array of the mark types to render.
   *                            If undefined, render all mark types
   */
  mark(el, scene, prev, markTypes) {
    if (!this.isDirty(scene)) {
      return scene._svg;
    }
    const svg = this._svg,
      markType = scene.marktype,
      mdef = Marks[markType],
      events = scene.interactive === false ? 'none' : null,
      isGroup = mdef.tag === 'g';
    const parent = bind$1(scene, el, prev, 'g', svg);
    if (markType !== 'group' && markTypes != null && !markTypes.includes(markType)) {
      domClear(parent, 0);
      return scene._svg;
    }
    parent.setAttribute('class', cssClass(scene));

    // apply aria attributes to parent container element
    const aria = ariaMarkAttributes(scene);
    for (const key in aria) setAttribute(parent, key, aria[key]);
    if (!isGroup) {
      setAttribute(parent, 'pointer-events', events);
    }
    setAttribute(parent, 'clip-path', scene.clip ? clip$1(this, scene, scene.group) : null);
    let sibling = null,
      i = 0;
    const process = item => {
      const dirty = this.isDirty(item),
        node = bind$1(item, parent, sibling, mdef.tag, svg);
      if (dirty) {
        this._update(mdef, node, item);
        if (isGroup) recurse(this, node, item, markTypes);
      }
      sibling = node;
      ++i;
    };
    if (mdef.nested) {
      if (scene.items.length) process(scene.items[0]);
    } else {
      visit(scene, process);
    }
    domClear(parent, i);
    return parent;
  }

  /**
   * Update the attributes of an SVG element for a mark item.
   * @param {object} mdef - The mark definition object
   * @param {SVGElement} el - The SVG element.
   * @param {Item} item - The mark item.
   */
  _update(mdef, el, item) {
    // set dom element and values cache
    // provides access to emit method
    element$1 = el;
    values$2 = el.__values__;

    // apply aria-specific properties
    ariaItemAttributes(emit, item);

    // apply svg attributes
    mdef.attr(emit, item, this);

    // some marks need special treatment
    const extra = mark_extras[mdef.type];
    if (extra) extra.call(this, mdef, el, item);

    // apply svg style attributes
    // note: element state may have been modified by 'extra' method
    if (element$1) this.style(element$1, item);
  }

  /**
   * Update the presentation attributes of an SVG element for a mark item.
   * @param {SVGElement} el - The SVG element.
   * @param {Item} item - The mark item.
   */
  style(el, item) {
    if (item == null) return;
    for (const prop in stylesAttr) {
      let value = prop === 'font' ? fontFamily(item) : item[prop];
      if (value === values$2[prop]) continue;
      const name = stylesAttr[prop];
      if (value == null) {
        el.removeAttribute(name);
      } else {
        if (isGradient(value)) {
          value = gradientRef(value, this._defs.gradient, href());
        }
        el.setAttribute(name, value + '');
      }
      values$2[prop] = value;
    }
    for (const prop in stylesCss) {
      setStyle(el, stylesCss[prop], item[prop]);
    }
  }

  /**
   * Render SVG defs, as needed.
   * Must be called *after* marks have been processed to ensure the
   * collected state is current and accurate.
   */
  defs() {
    const svg = this._svg,
      defs = this._defs;
    let el = defs.el,
      index = 0;
    for (const id in defs.gradient) {
      if (!el) defs.el = el = domChild(svg, RootIndex + 1, 'defs', svgns);
      index = updateGradient(el, defs.gradient[id], index);
    }
    for (const id in defs.clipping) {
      if (!el) defs.el = el = domChild(svg, RootIndex + 1, 'defs', svgns);
      index = updateClipping(el, defs.clipping[id], index);
    }

    // clean-up
    if (el) {
      index === 0 ? (svg.removeChild(el), defs.el = null) : domClear(el, index);
    }
  }

  /**
   * Clear defs caches.
   */
  _clearDefs() {
    const def = this._defs;
    def.gradient = {};
    def.clipping = {};
  }
}

// mark ancestor chain with a dirty id
function dirtyParents(item, id) {
  for (; item && item.dirty !== id; item = item.mark.group) {
    item.dirty = id;
    if (item.mark && item.mark.dirty !== id) {
      item.mark.dirty = id;
    } else return;
  }
}

// update gradient definitions
function updateGradient(el, grad, index) {
  let i, n, stop;
  if (grad.gradient === 'radial') {
    // SVG radial gradients automatically transform to normalized bbox
    // coordinates, in a way that is cumbersome to replicate in canvas.
    // We wrap the radial gradient in a pattern element, allowing us to
    // maintain a circular gradient that matches what canvas provides.
    let pt = domChild(el, index++, 'pattern', svgns);
    setAttributes(pt, {
      id: patternPrefix + grad.id,
      viewBox: '0,0,1,1',
      width: '100%',
      height: '100%',
      preserveAspectRatio: 'xMidYMid slice'
    });
    pt = domChild(pt, 0, 'rect', svgns);
    setAttributes(pt, {
      width: 1,
      height: 1,
      fill: `url(${href()}#${grad.id})`
    });
    el = domChild(el, index++, 'radialGradient', svgns);
    setAttributes(el, {
      id: grad.id,
      fx: grad.x1,
      fy: grad.y1,
      fr: grad.r1,
      cx: grad.x2,
      cy: grad.y2,
      r: grad.r2
    });
  } else {
    el = domChild(el, index++, 'linearGradient', svgns);
    setAttributes(el, {
      id: grad.id,
      x1: grad.x1,
      x2: grad.x2,
      y1: grad.y1,
      y2: grad.y2
    });
  }
  for (i = 0, n = grad.stops.length; i < n; ++i) {
    stop = domChild(el, i, 'stop', svgns);
    stop.setAttribute('offset', grad.stops[i].offset);
    stop.setAttribute('stop-color', grad.stops[i].color);
  }
  domClear(el, i);
  return index;
}

// update clipping path definitions
function updateClipping(el, clip, index) {
  let mask;
  el = domChild(el, index, 'clipPath', svgns);
  el.setAttribute('id', clip.id);
  if (clip.path) {
    mask = domChild(el, 0, 'path', svgns);
    mask.setAttribute('d', clip.path);
  } else {
    mask = domChild(el, 0, 'rect', svgns);
    setAttributes(mask, {
      x: 0,
      y: 0,
      width: clip.width,
      height: clip.height
    });
  }
  domClear(el, 1);
  return index + 1;
}

// Recursively process group contents.
function recurse(renderer, el, group, markTypes) {
  // child 'g' element is second to last among children (path, g, path)
  // other children here are foreground and background path elements
  el = el.lastChild.previousSibling;
  let prev,
    idx = 0;
  visit(group, item => {
    prev = renderer.mark(el, item, prev, markTypes);
    ++idx;
  });

  // remove any extraneous DOM elements
  domClear(el, 1 + idx);
}

// Bind a scenegraph item to an SVG DOM element.
// Create new SVG elements as needed.
function bind$1(item, el, sibling, tag, svg) {
  let node = item._svg,
    doc;

  // create a new dom node if needed
  if (!node) {
    doc = el.ownerDocument;
    node = domCreate(doc, tag, svgns);
    item._svg = node;
    if (item.mark) {
      node.__data__ = item;
      node.__values__ = {
        fill: 'default'
      };

      // if group, create background, content, and foreground elements
      if (tag === 'g') {
        const bg = domCreate(doc, 'path', svgns);
        node.appendChild(bg);
        bg.__data__ = item;
        const cg = domCreate(doc, 'g', svgns);
        node.appendChild(cg);
        cg.__data__ = item;
        const fg = domCreate(doc, 'path', svgns);
        node.appendChild(fg);
        fg.__data__ = item;
        fg.__values__ = {
          fill: 'default'
        };
      }
    }
  }

  // (re-)insert if (a) not contained in SVG or (b) sibling order has changed
  if (node.ownerSVGElement !== svg || siblingCheck(node, sibling)) {
    el.insertBefore(node, sibling ? sibling.nextSibling : el.firstChild);
  }
  return node;
}

// check if two nodes are ordered siblings
function siblingCheck(node, sibling) {
  return node.parentNode && node.parentNode.childNodes.length > 1 && node.previousSibling != sibling; // treat null/undefined the same
}

// -- Set attributes & styles on SVG elements ---

let element$1 = null,
  // temp var for current SVG element
  values$2 = null; // temp var for current values hash

// Extra configuration for certain mark types
const mark_extras = {
  group(mdef, el, item) {
    const fg = element$1 = el.childNodes[2];
    values$2 = fg.__values__;
    mdef.foreground(emit, item, this);
    values$2 = el.__values__; // use parent's values hash
    element$1 = el.childNodes[1];
    mdef.content(emit, item, this);
    const bg = element$1 = el.childNodes[0];
    mdef.background(emit, item, this);
    const value = item.mark.interactive === false ? 'none' : null;
    if (value !== values$2.events) {
      setAttribute(fg, 'pointer-events', value);
      setAttribute(bg, 'pointer-events', value);
      values$2.events = value;
    }
    if (item.strokeForeground && item.stroke) {
      const fill = item.fill;
      setAttribute(fg, 'display', null);

      // set style of background
      this.style(bg, item);
      setAttribute(bg, 'stroke', null);

      // set style of foreground
      if (fill) item.fill = null;
      values$2 = fg.__values__;
      this.style(fg, item);
      if (fill) item.fill = fill;

      // leave element null to prevent downstream styling
      element$1 = null;
    } else {
      // ensure foreground is ignored
      setAttribute(fg, 'display', 'none');
    }
  },
  image(mdef, el, item) {
    if (item.smooth === false) {
      setStyle(el, 'image-rendering', 'optimizeSpeed');
      setStyle(el, 'image-rendering', 'pixelated');
    } else {
      setStyle(el, 'image-rendering', null);
    }
  },
  text(mdef, el, item) {
    const tl = textLines(item);
    let key, value, doc, lh;
    if (isArray(tl)) {
      // multi-line text
      value = tl.map(_ => textValue(item, _));
      key = value.join('\n'); // content cache key

      if (key !== values$2.text) {
        domClear(el, 0);
        doc = el.ownerDocument;
        lh = lineHeight(item);
        value.forEach((t, i) => {
          const ts = domCreate(doc, 'tspan', svgns);
          ts.__data__ = item; // data binding
          ts.textContent = t;
          if (i) {
            ts.setAttribute('x', 0);
            ts.setAttribute('dy', lh);
          }
          el.appendChild(ts);
        });
        values$2.text = key;
      }
    } else {
      // single-line text
      value = textValue(item, tl);
      if (value !== values$2.text) {
        el.textContent = value;
        values$2.text = value;
      }
    }
    setAttribute(el, 'font-family', fontFamily(item));
    setAttribute(el, 'font-size', fontSize(item) + 'px');
    setAttribute(el, 'font-style', item.fontStyle);
    setAttribute(el, 'font-variant', item.fontVariant);
    setAttribute(el, 'font-weight', item.fontWeight);
  }
};
function emit(name, value, ns) {
  // early exit if value is unchanged
  if (value === values$2[name]) return;

  // use appropriate method given namespace (ns)
  if (ns) {
    setAttributeNS(element$1, name, value, ns);
  } else {
    setAttribute(element$1, name, value);
  }

  // note current value for future comparison
  values$2[name] = value;
}
function setStyle(el, name, value) {
  if (value !== values$2[name]) {
    if (value == null) {
      el.style.removeProperty(name);
    } else {
      el.style.setProperty(name, value + '');
    }
    values$2[name] = value;
  }
}
function setAttributes(el, attrs) {
  for (const key in attrs) {
    setAttribute(el, key, attrs[key]);
  }
}
function setAttribute(el, name, value) {
  if (value != null) {
    // if value is provided, update DOM attribute
    el.setAttribute(name, value);
  } else {
    // else remove DOM attribute
    el.removeAttribute(name);
  }
}
function setAttributeNS(el, name, value, ns) {
  if (value != null) {
    // if value is provided, update DOM attribute
    el.setAttributeNS(ns, name, value);
  } else {
    // else remove DOM attribute
    el.removeAttributeNS(ns, name);
  }
}
function href() {
  let loc;
  return typeof window === 'undefined' ? '' : (loc = window.location).hash ? loc.href.slice(0, -loc.hash.length) : loc.href;
}

class SVGStringRenderer extends Renderer {
  constructor(loader) {
    super(loader);
    this._text = null;
    this._defs = {
      gradient: {},
      clipping: {}
    };
  }

  /**
   * Returns the rendered SVG text string,
   * or null if rendering has not yet occurred.
   */
  svg() {
    return this._text;
  }

  /**
   * Internal rendering method.
   * @param {object} scene - The root mark of a scenegraph to render.
   */
  _render(scene) {
    const m = markup();

    // svg tag
    m.open('svg', extend({}, metadata, {
      class: 'marks',
      width: this._width * this._scale,
      height: this._height * this._scale,
      viewBox: `0 0 ${this._width} ${this._height}`
    }));

    // background, if defined
    const bg = this._bgcolor;
    if (bg && bg !== 'transparent' && bg !== 'none') {
      m.open('rect', {
        width: this._width,
        height: this._height,
        fill: bg
      }).close();
    }

    // root content group
    m.open('g', rootAttributes, {
      transform: 'translate(' + this._origin + ')'
    });
    this.mark(m, scene);
    m.close(); // </g>

    // defs
    this.defs(m);

    // get SVG text string
    this._text = m.close() + '';
    return this;
  }

  /**
   * Render a set of mark items.
   * @param {object} m - The markup context.
   * @param {object} scene - The mark parent to render.
   */
  mark(m, scene) {
    const mdef = Marks[scene.marktype],
      tag = mdef.tag,
      attrList = [ariaItemAttributes, mdef.attr];

    // render opening group tag
    m.open('g', {
      'class': cssClass(scene),
      'clip-path': scene.clip ? clip$1(this, scene, scene.group) : null
    }, ariaMarkAttributes(scene), {
      'pointer-events': tag !== 'g' && scene.interactive === false ? 'none' : null
    });

    // render contained elements
    const process = item => {
      const href = this.href(item);
      if (href) m.open('a', href);
      m.open(tag, this.attr(scene, item, attrList, tag !== 'g' ? tag : null));
      if (tag === 'text') {
        const tl = textLines(item);
        if (isArray(tl)) {
          // multi-line text
          const attrs = {
            x: 0,
            dy: lineHeight(item)
          };
          for (let i = 0; i < tl.length; ++i) {
            m.open('tspan', i ? attrs : null).text(textValue(item, tl[i])).close();
          }
        } else {
          // single-line text
          m.text(textValue(item, tl));
        }
      } else if (tag === 'g') {
        const fore = item.strokeForeground,
          fill = item.fill,
          stroke = item.stroke;
        if (fore && stroke) {
          item.stroke = null;
        }
        m.open('path', this.attr(scene, item, mdef.background, 'bgrect')).close();

        // recurse for group content
        m.open('g', this.attr(scene, item, mdef.content));
        visit(item, scene => this.mark(m, scene));
        m.close();
        if (fore && stroke) {
          if (fill) item.fill = null;
          item.stroke = stroke;
          m.open('path', this.attr(scene, item, mdef.foreground, 'bgrect')).close();
          if (fill) item.fill = fill;
        } else {
          m.open('path', this.attr(scene, item, mdef.foreground, 'bgfore')).close();
        }
      }
      m.close(); // </tag>
      if (href) m.close(); // </a>
    };
    if (mdef.nested) {
      if (scene.items && scene.items.length) process(scene.items[0]);
    } else {
      visit(scene, process);
    }

    // render closing group tag
    return m.close(); // </g>
  }

  /**
   * Get href attributes for a hyperlinked mark item.
   * @param {Item} item - The mark item.
   */
  href(item) {
    const href = item.href;
    let attr;
    if (href) {
      if (attr = this._hrefs && this._hrefs[href]) {
        return attr;
      } else {
        this.sanitizeURL(href).then(attr => {
          // rewrite to use xlink namespace
          attr['xlink:href'] = attr.href;
          attr.href = null;
          (this._hrefs || (this._hrefs = {}))[href] = attr;
        });
      }
    }
    return null;
  }

  /**
   * Get an object of SVG attributes for a mark item.
   * @param {object} scene - The mark parent.
   * @param {Item} item - The mark item.
   * @param {array|function} attrs - One or more attribute emitters.
   * @param {string} tag - The tag being rendered.
   */
  attr(scene, item, attrs, tag) {
    const object = {},
      emit = (name, value, ns, prefixed) => {
        object[prefixed || name] = value;
      };

    // apply mark specific attributes
    if (Array.isArray(attrs)) {
      attrs.forEach(fn => fn(emit, item, this));
    } else {
      attrs(emit, item, this);
    }

    // apply style attributes
    if (tag) {
      style(object, item, scene, tag, this._defs);
    }
    return object;
  }

  /**
   * Render SVG defs, as needed.
   * Must be called *after* marks have been processed to ensure the
   * collected state is current and accurate.
   * @param {object} m - The markup context.
   */
  defs(m) {
    const gradient = this._defs.gradient,
      clipping = this._defs.clipping,
      count = Object.keys(gradient).length + Object.keys(clipping).length;
    if (count === 0) return; // nothing to do

    m.open('defs');
    for (const id in gradient) {
      const def = gradient[id],
        stops = def.stops;
      if (def.gradient === 'radial') {
        // SVG radial gradients automatically transform to normalized bbox
        // coordinates, in a way that is cumbersome to replicate in canvas.
        // We wrap the radial gradient in a pattern element, allowing us to
        // maintain a circular gradient that matches what canvas provides.

        m.open('pattern', {
          id: patternPrefix + id,
          viewBox: '0,0,1,1',
          width: '100%',
          height: '100%',
          preserveAspectRatio: 'xMidYMid slice'
        });
        m.open('rect', {
          width: '1',
          height: '1',
          fill: 'url(#' + id + ')'
        }).close();
        m.close(); // </pattern>

        m.open('radialGradient', {
          id: id,
          fx: def.x1,
          fy: def.y1,
          fr: def.r1,
          cx: def.x2,
          cy: def.y2,
          r: def.r2
        });
      } else {
        m.open('linearGradient', {
          id: id,
          x1: def.x1,
          x2: def.x2,
          y1: def.y1,
          y2: def.y2
        });
      }
      for (let i = 0; i < stops.length; ++i) {
        m.open('stop', {
          offset: stops[i].offset,
          'stop-color': stops[i].color
        }).close();
      }
      m.close();
    }
    for (const id in clipping) {
      const def = clipping[id];
      m.open('clipPath', {
        id: id
      });
      if (def.path) {
        m.open('path', {
          d: def.path
        }).close();
      } else {
        m.open('rect', {
          x: 0,
          y: 0,
          width: def.width,
          height: def.height
        }).close();
      }
      m.close();
    }
    m.close();
  }
}

// Helper function for attr for style presentation attributes
function style(s, item, scene, tag, defs) {
  let styleList;
  if (item == null) return s;
  if (tag === 'bgrect' && scene.interactive === false) {
    s['pointer-events'] = 'none';
  }
  if (tag === 'bgfore') {
    if (scene.interactive === false) {
      s['pointer-events'] = 'none';
    }
    s.display = 'none';
    if (item.fill !== null) return s;
  }
  if (tag === 'image' && item.smooth === false) {
    styleList = ['image-rendering: optimizeSpeed;', 'image-rendering: pixelated;'];
  }
  if (tag === 'text') {
    s['font-family'] = fontFamily(item);
    s['font-size'] = fontSize(item) + 'px';
    s['font-style'] = item.fontStyle;
    s['font-variant'] = item.fontVariant;
    s['font-weight'] = item.fontWeight;
  }
  for (const prop in stylesAttr) {
    let value = item[prop];
    const name = stylesAttr[prop];
    if (value === 'transparent' && (name === 'fill' || name === 'stroke')) ; else if (value != null) {
      if (isGradient(value)) {
        value = gradientRef(value, defs.gradient, '');
      }
      s[name] = value;
    }
  }
  for (const prop in stylesCss) {
    const value = item[prop];
    if (value != null) {
      styleList = styleList || [];
      styleList.push(`${stylesCss[prop]}: ${value};`);
    }
  }
  if (styleList) {
    s.style = styleList.join(' ');
  }
  return s;
}

/**
 * @typedef {Object} HybridRendererOptions
 *
 * @property {string[]} [svgMarkTypes=['text']] - An array of SVG mark types to render
 *                                                in the SVG layer. All other mark types
 *                                                will be rendered in the Canvas layer.
 * @property {boolean} [svgOnTop=true] - Flag to determine if SVG should be rendered on top.
 * @property {boolean} [debug=false] - Flag to enable or disable debugging mode. When true,
 *                                     the top layer will be stacked below the bottom layer
 *                                     rather than overlaid on top.
 */

/** @type {HybridRendererOptions} */
const OPTS = {
  svgMarkTypes: ['text'],
  svgOnTop: true,
  debug: false
};

/**
 * Configure the HybridRenderer
 *
 * @param {HybridRendererOptions} options - HybridRenderer configuration options.
 */
function setHybridRendererOptions(options) {
  OPTS['svgMarkTypes'] = options.svgMarkTypes ?? ['text'];
  OPTS['svgOnTop'] = options.svgOnTop ?? true;
  OPTS['debug'] = options.debug ?? false;
}
class HybridRenderer extends Renderer {
  constructor(loader) {
    super(loader);
    this._svgRenderer = new SVGRenderer(loader);
    this._canvasRenderer = new CanvasRenderer(loader);
  }

  /**
   * Initialize a new HybridRenderer instance.
   * @param {DOMElement} el - The containing DOM element for the display.
   * @param {number} width - The coordinate width of the display, in pixels.
   * @param {number} height - The coordinate height of the display, in pixels.
   * @param {Array<number>} origin - The origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {number} [scaleFactor=1] - Optional scaleFactor by which to multiply
   *   the width and height to determine the final pixel size.
   * @return {HybridRenderer} - This renderer instance.
   */
  initialize(el, width, height, origin, scaleFactor) {
    this._root_el = domChild(el, 0, 'div');
    const bottomEl = domChild(this._root_el, 0, 'div');
    const topEl = domChild(this._root_el, 1, 'div');
    this._root_el.style.position = 'relative';

    // Set position absolute to overlay svg on top of canvas
    if (!OPTS.debug) {
      bottomEl.style.height = '100%';
      topEl.style.position = 'absolute';
      topEl.style.top = '0';
      topEl.style.left = '0';
      topEl.style.height = '100%';
      topEl.style.width = '100%';
    }
    this._svgEl = OPTS.svgOnTop ? topEl : bottomEl;
    this._canvasEl = OPTS.svgOnTop ? bottomEl : topEl;

    // pointer-events to none on SVG layer so that canvas gets all events
    this._svgEl.style.pointerEvents = 'none';
    this._canvasRenderer.initialize(this._canvasEl, width, height, origin, scaleFactor);
    this._svgRenderer.initialize(this._svgEl, width, height, origin, scaleFactor);
    return super.initialize(el, width, height, origin, scaleFactor);
  }

  /**
   * Flag a mark item as dirty.
   * @param {Item} item - The mark item.
   */
  dirty(item) {
    if (OPTS.svgMarkTypes.includes(item.mark.marktype)) {
      this._svgRenderer.dirty(item);
    } else {
      this._canvasRenderer.dirty(item);
    }
    return this;
  }

  /**
   * Internal rendering method.
   * @param {object} scene - The root mark of a scenegraph to render.
   * @param {Array} markTypes - Array of the mark types to render.
   *                            If undefined, render all mark types
   */
  _render(scene, markTypes) {
    const allMarkTypes = markTypes ?? ['arc', 'area', 'image', 'line', 'path', 'rect', 'rule', 'shape', 'symbol', 'text', 'trail'];
    const canvasMarkTypes = allMarkTypes.filter(m => !OPTS.svgMarkTypes.includes(m));
    this._svgRenderer.render(scene, OPTS.svgMarkTypes);
    this._canvasRenderer.render(scene, canvasMarkTypes);
  }

  /**
   * Resize the display.
   * @param {number} width - The new coordinate width of the display, in pixels.
   * @param {number} height - The new coordinate height of the display, in pixels.
   * @param {Array<number>} origin - The new origin of the display, in pixels.
   *   The coordinate system will be translated to this point.
   * @param {number} [scaleFactor=1] - Optional scaleFactor by which to multiply
   *   the width and height to determine the final pixel size.
   * @return {SVGRenderer} - This renderer instance;
   */
  resize(width, height, origin, scaleFactor) {
    super.resize(width, height, origin, scaleFactor);
    this._svgRenderer.resize(width, height, origin, scaleFactor);
    this._canvasRenderer.resize(width, height, origin, scaleFactor);
    return this;
  }
  background(bgcolor) {
    // Propagate background color to lower canvas renderer
    if (OPTS.svgOnTop) {
      this._canvasRenderer.background(bgcolor);
    } else {
      this._svgRenderer.background(bgcolor);
    }
    return this;
  }
}

class HybridHandler extends CanvasHandler {
  constructor(loader, tooltip) {
    super(loader, tooltip);
  }
  initialize(el, origin, obj) {
    const canvas = domChild(domChild(el, 0, 'div'), OPTS.svgOnTop ? 0 : 1, 'div');
    return super.initialize(canvas, origin, obj);
  }
}

const Canvas = 'canvas';
const Hybrid = 'hybrid';
const PNG = 'png';
const SVG = 'svg';
const None$1 = 'none';
const RenderType = {
  Canvas: Canvas,
  PNG: PNG,
  SVG: SVG,
  Hybrid: Hybrid,
  None: None$1
};
const modules = {};
modules[Canvas] = modules[PNG] = {
  renderer: CanvasRenderer,
  headless: CanvasRenderer,
  handler: CanvasHandler
};
modules[SVG] = {
  renderer: SVGRenderer,
  headless: SVGStringRenderer,
  handler: SVGHandler
};
modules[Hybrid] = {
  renderer: HybridRenderer,
  headless: HybridRenderer,
  handler: HybridHandler
};
modules[None$1] = {};
function renderModule(name, _) {
  name = String(name || '').toLowerCase();
  if (arguments.length > 1) {
    modules[name] = _;
    return this;
  } else {
    return modules[name];
  }
}

function intersect$2(scene, bounds, filter) {
  const hits = [],
    // intersection results
    box = new Bounds().union(bounds),
    // defensive copy
    type = scene.marktype;
  return type ? intersectMark(scene, box, filter, hits) : type === 'group' ? intersectGroup(scene, box, filter, hits) : error$1('Intersect scene must be mark node or group item.');
}
function intersectMark(mark, box, filter, hits) {
  if (visitMark(mark, box, filter)) {
    const items = mark.items,
      type = mark.marktype,
      n = items.length;
    let i = 0;
    if (type === 'group') {
      for (; i < n; ++i) {
        intersectGroup(items[i], box, filter, hits);
      }
    } else {
      for (const test = Marks[type].isect; i < n; ++i) {
        const item = items[i];
        if (intersectItem(item, box, test)) hits.push(item);
      }
    }
  }
  return hits;
}
function visitMark(mark, box, filter) {
  // process if bounds intersect and if
  // (1) mark is a group mark (so we must recurse), or
  // (2) mark is interactive and passes filter
  return mark.bounds && box.intersects(mark.bounds) && (mark.marktype === 'group' || mark.interactive !== false && (!filter || filter(mark)));
}
function intersectGroup(group, box, filter, hits) {
  // test intersect against group
  // skip groups by default unless filter says otherwise
  if (filter && filter(group.mark) && intersectItem(group, box, Marks.group.isect)) {
    hits.push(group);
  }

  // recursively test children marks
  // translate box to group coordinate space
  const marks = group.items,
    n = marks && marks.length;
  if (n) {
    const x = group.x || 0,
      y = group.y || 0;
    box.translate(-x, -y);
    for (let i = 0; i < n; ++i) {
      intersectMark(marks[i], box, filter, hits);
    }
    box.translate(x, y);
  }
  return hits;
}
function intersectItem(item, box, test) {
  // test bounds enclosure, bounds intersection, then detailed test
  const bounds = item.bounds;
  return box.encloses(bounds) || box.intersects(bounds) && test(item, box);
}

const clipBounds = new Bounds();
function boundClip (mark) {
  const clip = mark.clip;
  if (isFunction(clip)) {
    clip(boundContext(clipBounds.clear()));
  } else if (clip) {
    clipBounds.set(0, 0, mark.group.width, mark.group.height);
  } else return;
  mark.bounds.intersect(clipBounds);
}

const TOLERANCE = 1e-9;
function sceneEqual(a, b, key) {
  return a === b ? true : key === 'path' ? pathEqual(a, b) : a instanceof Date && b instanceof Date ? +a === +b : isNumber$1(a) && isNumber$1(b) ? Math.abs(a - b) <= TOLERANCE : !a || !b || !isObject(a) && !isObject(b) ? a == b : objectEqual(a, b);
}
function pathEqual(a, b) {
  return sceneEqual(parse$3(a), parse$3(b));
}
function objectEqual(a, b) {
  var ka = Object.keys(a),
    kb = Object.keys(b),
    key,
    i;
  if (ka.length !== kb.length) return false;
  ka.sort();
  kb.sort();
  for (i = ka.length - 1; i >= 0; i--) {
    if (ka[i] != kb[i]) return false;
  }
  for (i = ka.length - 1; i >= 0; i--) {
    key = ka[i];
    if (!sceneEqual(a[key], b[key], key)) return false;
  }
  return typeof a === typeof b;
}

function resetSVGDefIds() {
  resetSVGClipId();
  resetSVGGradientId();
}

const Top$1 = 'top';
const Left$1 = 'left';
const Right$1 = 'right';
const Bottom$1 = 'bottom';
const TopLeft = 'top-left';
const TopRight = 'top-right';
const BottomLeft = 'bottom-left';
const BottomRight = 'bottom-right';
const Start$1 = 'start';
const Middle$1 = 'middle';
const End$1 = 'end';
const X$1 = 'x';
const Y$1 = 'y';
const Group = 'group';
const AxisRole$1 = 'axis';
const TitleRole$1 = 'title';
const FrameRole$1 = 'frame';
const ScopeRole$1 = 'scope';
const LegendRole$1 = 'legend';
const RowHeader = 'row-header';
const RowFooter = 'row-footer';
const RowTitle = 'row-title';
const ColHeader = 'column-header';
const ColFooter = 'column-footer';
const ColTitle = 'column-title';
const Padding$1 = 'padding';
const Symbols$1 = 'symbol';
const Fit = 'fit';
const FitX = 'fit-x';
const FitY = 'fit-y';
const Pad = 'pad';
const None = 'none';
const All = 'all';
const Each = 'each';
const Flush = 'flush';
const Column = 'column';
const 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$1(params) {
  Transform.call(this, null, params);
}
inherits(Bound$1, Transform, {
  transform(_, pulse) {
    const view = pulse.dataflow,
      mark = _.mark,
      type = mark.marktype,
      entry = Marks[type],
      bound = entry.bound;
    let 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(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, item => view.dirty(item));
      markBounds.clear();
      mark.items.forEach(item => markBounds.union(boundItem(item, bound)));

      // force reflow for axes/legends/titles to propagate any layout changes
      switch (mark.role) {
        case AxisRole$1:
        case LegendRole$1:
        case TitleRole$1:
          pulse.reflow();
      }
    } else {
      // incrementally update bounds, re-bound mark as needed
      rebound = pulse.changed(pulse.REM);
      pulse.visit(pulse.ADD, item => {
        markBounds.union(boundItem(item, bound));
      });
      pulse.visit(pulse.MOD, item => {
        rebound = rebound || markBounds.alignsWith(item.bounds);
        view.dirty(item);
        markBounds.union(boundItem(item, bound));
      });
      if (rebound) {
        markBounds.clear();
        mark.items.forEach(item => markBounds.union(item.bounds));
      }
    }

    // ensure mark bounds do not exceed any clipping region
    boundClip(mark);
    return pulse.modifies('bounds');
  }
});
function boundItem(item, bound, opt) {
  return bound(item.bounds.clear(), item, opt);
}

const 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$1(params) {
  Transform.call(this, 0, params);
}
Identifier$1.Definition = {
  'type': 'Identifier',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'as',
    'type': 'string',
    'required': true
  }]
};
inherits(Identifier$1, Transform, {
  transform(_, pulse) {
    const counter = getCounter(pulse.dataflow),
      as = _.as;
    let id = counter.value;
    pulse.visit(pulse.ADD, t => t[as] = t[as] || ++id);
    counter.set(this.value = id);
    return pulse;
  }
});
function getCounter(view) {
  return view._signals[COUNTER_NAME] || (view._signals[COUNTER_NAME] = view.add(0));
}

/**
 * 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$2(params) {
  Transform.call(this, null, params);
}
inherits(Mark$2, Transform, {
  transform(_, pulse) {
    let mark = this.value;

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

    // initialize entering items
    const Init = mark.marktype === Group ? GroupItem : Item;
    pulse.visit(pulse.ADD, 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$1$1(_) {
  const 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$1(params) {
  Transform.call(this, null, params);
}
const methods = {
  parity: items => items.filter((item, i) => i % 2 ? item.opacity = 0 : 1),
  greedy: (items, sep) => {
    let a;
    return items.filter((b, i) => !i || !intersect$1(a.bounds, b.bounds, sep) ? (a = b, 1) : b.opacity = 0);
  }
};

// compute bounding box intersection
// including padding pixels of separation
const intersect$1 = (a, b, sep) => sep > Math.max(b.x1 - a.x2, a.x1 - b.x2, b.y1 - a.y2, a.y1 - b.y2);
const hasOverlap = (items, pad) => {
  for (var i = 1, n = items.length, a = items[0].bounds, b; i < n; a = b, ++i) {
    if (intersect$1(a, b = items[i].bounds, pad)) return true;
  }
};
const hasBounds = item => {
  const b = item.bounds;
  return b.width() > 1 && b.height() > 1;
};
const boundTest = (scale, orient, tolerance) => {
  var range = scale.range(),
    b = new Bounds();
  if (orient === Top$1 || orient === Bottom$1) {
    b.set(range[0], -Infinity, range[1], +Infinity);
  } else {
    b.set(-Infinity, range[0], +Infinity, range[1]);
  }
  b.expand(tolerance || 1);
  return item => b.encloses(item.bounds);
};

// reset all items to be fully opaque
const reset$1 = source => {
  source.forEach(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)
const reflow = (pulse, _) => pulse.reflow(_.modified()).modifies('opacity');
inherits(Overlap$1, Transform, {
  transform(_, pulse) {
    const reduce = methods[_.method] || methods.parity,
      sep = _.separation || 0;
    let source = pulse.materialize(pulse.SOURCE).source,
      items,
      test;
    if (!source || !source.length) return;
    if (!_.method) {
      // early exit if method is falsy
      if (_.modified('method')) {
        reset$1(source);
        pulse = reflow(pulse, _);
      }
      return pulse;
    }

    // skip labels with no content
    source = source.filter(hasBounds);

    // early exit, nothing to do
    if (!source.length) return;
    if (_.sort) {
      source = source.slice().sort(_.sort);
    }
    items = reset$1(source);
    pulse = reflow(pulse, _);
    if (items.length >= 3 && hasOverlap(items, sep)) {
      do {
        items = reduce(items, sep);
      } while (items.length >= 3 && hasOverlap(items, sep));
      if (items.length < 3 && !peek$1(source).opacity) {
        if (items.length > 1) peek$1(items).opacity = 0;
        peek$1(source).opacity = 1;
      }
    }
    if (_.boundScale && _.boundTolerance >= 0) {
      test = boundTest(_.boundScale, _.boundOrient, +_.boundTolerance);
      source.forEach(item => {
        if (!test(item)) item.opacity = 0;
      });
    }

    // re-calculate mark bounds
    const bounds = items[0].mark.bounds.clear();
    source.forEach(item => {
      if (item.opacity) bounds.union(item.bounds);
    });
    return pulse;
  }
});

/**
 * Queue modified scenegraph items for rendering.
 * @constructor
 */
function Render$1(params) {
  Transform.call(this, null, params);
}
inherits(Render$1, Transform, {
  transform(_, pulse) {
    const view = pulse.dataflow;
    pulse.visit(pulse.ALL, item => view.dirty(item));

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

const tempBounds = new Bounds();
function set$2(item, property, value) {
  return item[property] === value ? 0 : (item[property] = value, 1);
}

function isYAxis(mark) {
  var orient = mark.items[0].orient;
  return orient === Left$1 || orient === Right$1;
}
function axisIndices(datum) {
  let 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,
    delta = item.translate != null ? item.translate : 0.5,
    orient = item.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,
    dl = title && multiLineOffset(title),
    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$1:
      x = position || 0;
      y = -offset;
      s = Math.max(minExtent, Math.min(maxExtent, -bounds.y1));
      bounds.add(0, -s).add(range, 0);
      if (title) axisTitleLayout(view, title, s, titlePadding, dl, 0, -1, bounds);
      break;
    case Left$1:
      x = -offset;
      y = position || 0;
      s = Math.max(minExtent, Math.min(maxExtent, -bounds.x1));
      bounds.add(-s, 0).add(0, range);
      if (title) axisTitleLayout(view, title, s, titlePadding, dl, 1, -1, bounds);
      break;
    case Right$1:
      x = width + offset;
      y = position || 0;
      s = Math.max(minExtent, Math.min(maxExtent, bounds.x2));
      bounds.add(0, 0).add(s, range);
      if (title) axisTitleLayout(view, title, s, titlePadding, dl, 1, 1, bounds);
      break;
    case Bottom$1:
      x = position || 0;
      y = height + offset;
      s = Math.max(minExtent, Math.min(maxExtent, bounds.y2));
      bounds.add(0, 0).add(range, s);
      if (title) axisTitleLayout(view, title, s, titlePadding, 0, 0, 1, bounds);
      break;
    default:
      x = item.x;
      y = item.y;
  }

  // update bounds
  boundStroke(bounds.translate(x, y), item);
  if (set$2(item, 'x', x + delta) | set$2(item, 'y', y + delta)) {
    item.bounds = tempBounds;
    view.dirty(item);
    item.bounds = bounds;
    view.dirty(item);
  }
  return item.mark.bounds.clear().union(bounds);
}
function axisTitleLayout(view, title, offset, pad, dl, isYAxis, sign, bounds) {
  const b = title.bounds;
  if (title.auto) {
    const v = sign * (offset + dl + pad);
    let dx = 0,
      dy = 0;
    view.dirty(title);
    isYAxis ? dx = (title.x || 0) - (title.x = v) : dy = (title.y || 0) - (title.y = v);
    title.mark.bounds.clear().union(b.translate(-dx, -dy));
    view.dirty(title);
  }
  bounds.union(b);
}

// aggregation functions for grid margin determination
const min = (a, b) => Math.floor(Math.min(a, b));
const max = (a, b) => Math.ceil(Math.max(a, b));
function gridLayoutGroups(group) {
  var groups = group.items,
    n = groups.length,
    i = 0,
    mark,
    items;
  const 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$1:
        case LegendRole$1:
        case TitleRole$1:
          break;
        case RowHeader:
          views.rowheaders.push(...items);
          break;
        case RowFooter:
          views.rowfooters.push(...items);
          break;
        case ColHeader:
          views.colheaders.push(...items);
          break;
        case ColFooter:
          views.colfooters.push(...items);
          break;
        case RowTitle:
          views.rowtitle = items[0];
          break;
        case ColTitle:
          views.coltitle = items[0];
          break;
        default:
          views.marks.push(...items);
      }
    }
  }
  return views;
}
function bboxFlush(item) {
  return new Bounds().set(0, 0, item.width || 0, item.height || 0);
}
function bboxFull(item) {
  const b = item.bounds.clone();
  return b.empty() ? b.set(0, 0, 0, 0) : b.translate(-(item.x || 0), -(item.y || 0));
}
function get(opt, key, d) {
  const v = isObject(opt) ? opt[key] : opt;
  return v != null ? v : d !== undefined ? d : 0;
}
function offsetValue$1(v) {
  return v < 0 ? Math.ceil(-v) : 0;
}
function gridLayout(view, groups, opt) {
  var dirty = !opt.nodirty,
    bbox = opt.bounds === Flush ? bboxFlush : bboxFull,
    bounds = tempBounds.set(0, 0, 0, 0),
    alignCol = get(opt.align, Column),
    alignRow = get(opt.align, Row),
    padCol = get(opt.padding, Column),
    padRow = get(opt.padding, Row),
    ncols = opt.columns || groups.length,
    nrows = ncols <= 0 ? 1 : Math.ceil(groups.length / ncols),
    n = groups.length,
    xOffset = Array(n),
    xExtent = Array(ncols),
    xMax = 0,
    yOffset = Array(n),
    yExtent = Array(nrows),
    yMax = 0,
    dx = Array(n),
    dy = Array(n),
    boxes = Array(n),
    m,
    i,
    c,
    r,
    b,
    g,
    px,
    py,
    x,
    y,
    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) {
    g = groups[i];
    b = boxes[i] = bbox(g);
    g.x = g.x || 0;
    dx[i] = 0;
    g.y = g.y || 0;
    dy[i] = 0;
    c = i % ncols;
    r = ~~(i / ncols);
    xMax = Math.max(xMax, px = Math.ceil(b.x2));
    yMax = Math.max(yMax, py = Math.ceil(b.y2));
    xExtent[c] = Math.max(xExtent[c], px);
    yExtent[r] = Math.max(yExtent[r], py);
    xOffset[i] = padCol + offsetValue$1(b.x1);
    yOffset[i] = padRow + offsetValue$1(b.y1);
    if (dirty) view.dirty(groups[i]);
  }

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

  // 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) {
    x = xOffset[i] + (i % ncols ? x : 0);
    dx[i] += x - groups[i].x;
  }

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

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

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

  // position grid relative to anchor
  for (i = 0; i < n; ++i) {
    bounds.union(boxes[i].translate(dx[i], dy[i]));
  }
  x = get(opt.anchor, X$1);
  y = get(opt.anchor, Y$1);
  switch (get(opt.anchor, Column)) {
    case End$1:
      x -= bounds.width();
      break;
    case Middle$1:
      x -= bounds.width() / 2;
  }
  switch (get(opt.anchor, Row)) {
    case End$1:
      y -= bounds.height();
      break;
    case Middle$1:
      y -= bounds.height() / 2;
  }
  x = Math.round(x);
  y = Math.round(y);

  // update mark positions, bounds, dirty
  bounds.clear();
  for (i = 0; i < n; ++i) {
    groups[i].mark.bounds.clear();
  }
  for (i = 0; i < n; ++i) {
    g = groups[i];
    g.x += dx[i] += x;
    g.y += dy[i] += y;
    bounds.union(g.mark.bounds.union(g.bounds.translate(dx[i], dy[i])));
    if (dirty) view.dirty(g);
  }
  return bounds;
}
function trellisLayout(view, group, opt) {
  var views = gridLayoutGroups(group),
    groups = views.marks,
    bbox = opt.bounds === Flush ? boundFlush : boundFull,
    off = opt.offset,
    ncols = opt.columns || groups.length,
    nrows = ncols <= 0 ? 1 : Math.ceil(groups.length / ncols),
    cells = nrows * ncols,
    x,
    y,
    x2,
    y2,
    anchor,
    band,
    offset;

  // -- initial grid layout
  const bounds = gridLayout(view, groups, opt);
  if (bounds.empty()) bounds.set(0, 0, 0, 0); // empty grid

  // -- layout grid headers and footers --

  // perform row header layout
  if (views.rowheaders) {
    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
  if (views.colheaders) {
    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
  if (views.rowfooters) {
    band = get(opt.footerBand, Row, null);
    x2 = layoutHeaders(view, views.rowfooters, groups, ncols, nrows, get(off, 'rowFooter'), max, 0, bbox, 'x2', ncols - 1, ncols, 1, band);
  }

  // perform column footer layout
  if (views.colfooters) {
    band = get(opt.footerBand, Column, null);
    y2 = 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) {
    anchor = get(opt.titleAnchor, Row);
    offset = get(off, 'rowTitle');
    offset = anchor === End$1 ? x2 + offset : x - offset;
    band = get(opt.titleBand, Row, 0.5);
    layoutTitle(view, views.rowtitle, offset, 0, bounds, band);
  }

  // perform column title layout
  if (views.coltitle) {
    anchor = get(opt.titleAnchor, Column);
    offset = get(off, 'columnTitle');
    offset = anchor === End$1 ? y2 + offset : y - offset;
    band = get(opt.titleBand, Column, 0.5);
    layoutTitle(view, views.coltitle, offset, 1, bounds, band);
  }
}
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 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);
}

// utility for looking up legend layout configuration
function lookup$3(config, orient) {
  const opt = config[orient] || {};
  return (key, d) => opt[key] != null ? opt[key] : config[key] != null ? config[key] : d;
}

// if legends specify offset directly, use the maximum specified value
function offsets(legends, value) {
  let max = -Infinity;
  legends.forEach(item => {
    if (item.offset != null) max = Math.max(max, item.offset);
  });
  return max > -Infinity ? max : value;
}
function legendParams(g, orient, config, xb, yb, w, h) {
  const _ = lookup$3(config, orient),
    offset = offsets(g, _('offset', 0)),
    anchor = _('anchor', Start$1),
    mult = anchor === End$1 ? 1 : anchor === Middle$1 ? 0.5 : 0;
  const p = {
    align: Each,
    bounds: _('bounds', Flush),
    columns: _('direction') === 'vertical' ? 1 : g.length,
    padding: _('margin', 8),
    center: _('center'),
    nodirty: true
  };
  switch (orient) {
    case Left$1:
      p.anchor = {
        x: Math.floor(xb.x1) - offset,
        column: End$1,
        y: mult * (h || xb.height() + 2 * xb.y1),
        row: anchor
      };
      break;
    case Right$1:
      p.anchor = {
        x: Math.ceil(xb.x2) + offset,
        y: mult * (h || xb.height() + 2 * xb.y1),
        row: anchor
      };
      break;
    case Top$1:
      p.anchor = {
        y: Math.floor(yb.y1) - offset,
        row: End$1,
        x: mult * (w || yb.width() + 2 * yb.x1),
        column: anchor
      };
      break;
    case Bottom$1:
      p.anchor = {
        y: Math.ceil(yb.y2) + offset,
        x: mult * (w || yb.width() + 2 * yb.x1),
        column: anchor
      };
      break;
    case TopLeft:
      p.anchor = {
        x: offset,
        y: offset
      };
      break;
    case TopRight:
      p.anchor = {
        x: w - offset,
        y: offset,
        column: End$1
      };
      break;
    case BottomLeft:
      p.anchor = {
        x: offset,
        y: h - offset,
        row: End$1
      };
      break;
    case BottomRight:
      p.anchor = {
        x: w - offset,
        y: h - offset,
        column: End$1,
        row: End$1
      };
      break;
  }
  return p;
}
function legendLayout(view, legend) {
  var item = legend.items[0],
    datum = item.datum,
    orient = item.orient,
    bounds = item.bounds,
    x = item.x,
    y = item.y,
    w,
    h;

  // cache current bounds for later comparison
  item._bounds ? item._bounds.clear().union(bounds) : item._bounds = bounds.clone();
  bounds.clear();

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

  // aggregate bounds to determine size, and include origin
  bounds = legendBounds(item, bounds);
  w = 2 * item.padding;
  h = 2 * item.padding;
  if (!bounds.empty()) {
    w = Math.ceil(bounds.width() + w);
    h = Math.ceil(bounds.height() + h);
  }
  if (datum.type === Symbols$1) {
    legendEntryLayout(item.items[0].items[0].items[0].items);
  }
  if (orient !== None) {
    item.x = x = 0;
    item.y = y = 0;
  }
  item.width = w;
  item.height = h;
  boundStroke(bounds.set(x, y, x + w, y + h), item);
  item.mark.bounds.clear().union(bounds);
  return item;
}
function legendBounds(item, b) {
  // aggregate item bounds
  item.items.forEach(_ => b.union(_.bounds));

  // anchor to legend origin
  b.x1 = item.padding;
  b.y1 = item.padding;
  return b;
}
function legendGroupLayout(view, item, entry) {
  var pad = item.padding,
    ex = pad - entry.x,
    ey = pad - entry.y;
  if (!item.datum.title) {
    if (ex || ey) translate$1(view, entry, ex, ey);
  } else {
    var title = item.items[1].items[0],
      anchor = title.anchor,
      tpad = item.titlePadding || 0,
      tx = pad - title.x,
      ty = pad - title.y;
    switch (title.orient) {
      case Left$1:
        ex += Math.ceil(title.bounds.width()) + tpad;
        break;
      case Right$1:
      case Bottom$1:
        break;
      default:
        ey += title.bounds.height() + tpad;
    }
    if (ex || ey) translate$1(view, entry, ex, ey);
    switch (title.orient) {
      case Left$1:
        ty += legendTitleOffset(item, entry, title, anchor, 1, 1);
        break;
      case Right$1:
        tx += legendTitleOffset(item, entry, title, End$1, 0, 0) + tpad;
        ty += legendTitleOffset(item, entry, title, anchor, 1, 1);
        break;
      case Bottom$1:
        tx += legendTitleOffset(item, entry, title, anchor, 0, 0);
        ty += legendTitleOffset(item, entry, title, End$1, -1, 0, 1) + tpad;
        break;
      default:
        tx += legendTitleOffset(item, entry, title, anchor, 0, 0);
    }
    if (tx || ty) translate$1(view, title, tx, ty);

    // translate legend if title pushes into negative coordinates
    if ((tx = Math.round(title.bounds.x1 - pad)) < 0) {
      translate$1(view, entry, -tx, 0);
      translate$1(view, title, -tx, 0);
    }
  }
}
function legendTitleOffset(item, entry, title, anchor, y, lr, noBar) {
  const grad = item.datum.type !== 'symbol',
    vgrad = title.datum.vgrad,
    e = grad && (lr || !vgrad) && !noBar ? entry.items[0] : entry,
    s = e.bounds[y ? 'y2' : 'x2'] - item.padding,
    u = vgrad && lr ? s : 0,
    v = vgrad && lr ? 0 : s,
    o = y <= 0 ? 0 : multiLineOffset(title);
  return Math.round(anchor === Start$1 ? u : anchor === End$1 ? v - o : 0.5 * (s - o));
}
function translate$1(view, item, dx, dy) {
  item.x += dx;
  item.y += dy;
  item.bounds.translate(dx, dy);
  item.mark.bounds.translate(dx, dy);
  view.dirty(item);
}
function legendEntryLayout(entries) {
  // get max widths for each column
  const widths = entries.reduce((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(g => {
    g.width = widths[g.column];
    g.height = g.bounds.y2 - g.y;
  });
}

function titleLayout(view, mark, width, height, viewBounds) {
  var group = mark.items[0],
    frame = group.frame,
    orient = group.orient,
    anchor = group.anchor,
    offset = group.offset,
    padding = group.padding,
    title = group.items[0].items[0],
    subtitle = group.items[1] && group.items[1].items[0],
    end = orient === Left$1 || orient === Right$1 ? height : width,
    start = 0,
    x = 0,
    y = 0,
    sx = 0,
    sy = 0,
    pos;
  if (frame !== Group) {
    orient === Left$1 ? (start = viewBounds.y2, end = viewBounds.y1) : orient === Right$1 ? (start = viewBounds.y1, end = viewBounds.y2) : (start = viewBounds.x1, end = viewBounds.x2);
  } else if (orient === Left$1) {
    start = height, end = 0;
  }
  pos = anchor === Start$1 ? start : anchor === End$1 ? end : (start + end) / 2;
  if (subtitle && subtitle.text) {
    // position subtitle
    switch (orient) {
      case Top$1:
      case Bottom$1:
        sy = title.bounds.height() + padding;
        break;
      case Left$1:
        sx = title.bounds.width() + padding;
        break;
      case Right$1:
        sx = -title.bounds.width() - padding;
        break;
    }
    tempBounds.clear().union(subtitle.bounds);
    tempBounds.translate(sx - (subtitle.x || 0), sy - (subtitle.y || 0));
    if (set$2(subtitle, 'x', sx) | set$2(subtitle, 'y', sy)) {
      view.dirty(subtitle);
      subtitle.bounds.clear().union(tempBounds);
      subtitle.mark.bounds.clear().union(tempBounds);
      view.dirty(subtitle);
    }
    tempBounds.clear().union(subtitle.bounds);
  } else {
    tempBounds.clear();
  }
  tempBounds.union(title.bounds);

  // position title group
  switch (orient) {
    case Top$1:
      x = pos;
      y = viewBounds.y1 - tempBounds.height() - offset;
      break;
    case Left$1:
      x = viewBounds.x1 - tempBounds.width() - offset;
      y = pos;
      break;
    case Right$1:
      x = viewBounds.x2 + tempBounds.width() + offset;
      y = pos;
      break;
    case Bottom$1:
      x = pos;
      y = viewBounds.y2 + offset;
      break;
    default:
      x = group.x;
      y = group.y;
  }
  if (set$2(group, 'x', x) | set$2(group, 'y', y)) {
    tempBounds.translate(x, y);
    view.dirty(group);
    group.bounds.clear().union(tempBounds);
    mark.bounds.clear().union(tempBounds);
    view.dirty(group);
  }
  return group.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$1(params) {
  Transform.call(this, null, params);
}
inherits(ViewLayout$1, Transform, {
  transform(_, pulse) {
    const view = pulse.dataflow;
    _.mark.items.forEach(group => {
      if (_.layout) trellisLayout(view, group, _.layout);
      layoutGroup(view, group, _);
    });
    return shouldReflow(_.mark.group) ? pulse.reflow() : pulse;
  }
});
function shouldReflow(group) {
  // We typically should reflow if layout is invoked (#2568), as child items
  // may have resized and reflow ensures group bounds are re-calculated.
  // However, legend entries have a special exception to avoid instability.
  // For example, if a selected legend symbol gains a stroke on hover,
  // we don't want to re-position subsequent elements in the legend.
  return group && group.mark.role !== 'legend-entry';
}
function layoutGroup(view, group, _) {
  var items = group.items,
    width = Math.max(0, group.width || 0),
    height = Math.max(0, group.height || 0),
    viewBounds = new Bounds().set(0, 0, width, height),
    xBounds = viewBounds.clone(),
    yBounds = viewBounds.clone(),
    legends = [],
    title,
    mark,
    orient,
    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$1:
        b = isYAxis(mark) ? xBounds : yBounds;
        b.union(axisLayout(view, mark, width, height));
        break;
      case TitleRole$1:
        title = mark;
        break;
      case LegendRole$1:
        legends.push(legendLayout(view, mark));
        break;
      case FrameRole$1:
      case ScopeRole$1:
      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) {
    // group legends by orient
    const l = {};
    legends.forEach(item => {
      orient = item.orient || Right$1;
      if (orient !== None) (l[orient] || (l[orient] = [])).push(item);
    });

    // perform grid layout for each orient group
    for (const orient in l) {
      const g = l[orient];
      gridLayout(view, g, legendParams(g, orient, _.legends, xBounds, yBounds, width, height));
    }

    // update view bounds
    legends.forEach(item => {
      const b = item.bounds;
      if (!b.equals(item._bounds)) {
        item.bounds = item._bounds;
        view.dirty(item); // dirty previous location
        item.bounds = b;
        view.dirty(item);
      }
      if (_.autosize && (_.autosize.type === Fit || _.autosize.type === FitX || _.autosize.type === FitY)) {
        // 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!
        switch (item.orient) {
          case Left$1:
          case Right$1:
            viewBounds.add(b.x1, 0).add(b.x2, 0);
            break;
          case Top$1:
          case Bottom$1:
            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));
  }

  // override aggregated view bounds if content is clipped
  if (group.clip) {
    viewBounds.set(0, 0, group.width || 0, group.height || 0);
  }

  // perform size adjustment
  viewSizeLayout(view, group, viewBounds, _);
}
function viewSizeLayout(view, group, viewBounds, _) {
  const auto = _.autosize || {},
    type = auto.type;
  if (view._autosize < 1 || !type) return;
  let viewWidth = view._width,
    viewHeight = view._height,
    width = Math.max(0, group.width || 0),
    left = Math.max(0, Math.ceil(-viewBounds.x1)),
    height = Math.max(0, group.height || 0),
    top = Math.max(0, Math.ceil(-viewBounds.y1));
  const right = Math.max(0, Math.ceil(viewBounds.x2 - width)),
    bottom = Math.max(0, Math.ceil(viewBounds.y2 - height));
  if (auto.contains === Padding$1) {
    const padding = view.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);
}

const vtx = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  bound: Bound$1,
  identifier: Identifier$1,
  mark: Mark$2,
  overlap: Overlap$1,
  render: Render$1,
  viewlayout: ViewLayout$1
}, Symbol.toStringTag, { value: 'Module' }));

/**
 * Generates axis ticks for visualizing a spatial scale.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Scale} params.scale - The scale to generate ticks for.
 * @param {*} [params.count=10] - The approximate number of ticks, or
 *   desired tick interval, to use.
 * @param {Array<*>} [params.values] - The exact tick values to use.
 *   These must be legal domain values for the provided scale.
 *   If provided, the count argument is ignored.
 * @param {function(*):string} [params.formatSpecifier] - A format specifier
 *   to use in conjunction with scale.tickFormat. Legal values are
 *   any valid d3 4.0 format specifier.
 * @param {function(*):string} [params.format] - The format function to use.
 *   If provided, the formatSpecifier argument is ignored.
 */
function AxisTicks$1(params) {
  Transform.call(this, null, params);
}
inherits(AxisTicks$1, Transform, {
  transform(_, pulse) {
    if (this.value && !_.modified()) {
      return pulse.StopPropagation;
    }
    var locale = pulse.dataflow.locale(),
      out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      ticks = this.value,
      scale = _.scale,
      tally = _.count == null ? _.values ? _.values.length : 10 : _.count,
      count = tickCount(scale, tally, _.minstep),
      format = _.format || tickFormat(locale, scale, count, _.formatSpecifier, _.formatType, !!_.values),
      values = _.values ? validTicks(scale, _.values, count) : tickValues(scale, count);
    if (ticks) out.rem = ticks;
    ticks = values.map((value, i) => ingest$1({
      index: i / (values.length - 1 || 1),
      value: value,
      label: format(value)
    }));
    if (_.extra && ticks.length) {
      // add an extra tick pegged to the initial domain value
      // this is used to generate axes with 'binned' domains
      ticks.push(ingest$1({
        index: -1,
        extra: {
          value: ticks[0].value
        },
        label: ''
      }));
    }
    out.source = ticks;
    out.add = ticks;
    this.value = ticks;
    return out;
  }
});

/**
 * Joins a set of data elements against a set of visual items.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): object} [params.item] - An item generator function.
 * @param {function(object): *} [params.key] - The key field associating data and visual items.
 */
function DataJoin$1(params) {
  Transform.call(this, null, params);
}
function defaultItemCreate() {
  return ingest$1({});
}
function newMap(key) {
  const map = fastmap().test(t => t.exit);
  map.lookup = t => map.get(key(t));
  return map;
}
inherits(DataJoin$1, Transform, {
  transform(_, pulse) {
    var df = pulse.dataflow,
      out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      item = _.item || defaultItemCreate,
      key = _.key || tupleid,
      map = this.value;

    // prevent transient (e.g., hover) requests from
    // cascading across marks derived from marks
    if (isArray(out.encode)) {
      out.encode = null;
    }
    if (map && (_.modified('key') || pulse.modified(key))) {
      error$1('DataJoin does not support modified key function or fields.');
    }
    if (!map) {
      pulse = pulse.addAll();
      this.value = map = newMap(key);
    }
    pulse.visit(pulse.ADD, t => {
      const k = key(t);
      let x = map.get(k);
      if (x) {
        if (x.exit) {
          map.empty--;
          out.add.push(x);
        } else {
          out.mod.push(x);
        }
      } else {
        x = item(t);
        map.set(k, x);
        out.add.push(x);
      }
      x.datum = t;
      x.exit = false;
    });
    pulse.visit(pulse.MOD, t => {
      const k = key(t),
        x = map.get(k);
      if (x) {
        x.datum = t;
        out.mod.push(x);
      }
    });
    pulse.visit(pulse.REM, t => {
      const k = key(t),
        x = map.get(k);
      if (t === x.datum && !x.exit) {
        out.rem.push(x);
        x.exit = true;
        ++map.empty;
      }
    });
    if (pulse.changed(pulse.ADD_MOD)) out.modifies('datum');
    if (pulse.clean() || _.clean && map.empty > df.cleanThreshold) {
      df.runAfter(map.clean);
    }
    return out;
  }
});

/**
 * Invokes encoding functions for visual items.
 * @constructor
 * @param {object} params - The parameters to the encoding functions. This
 *   parameter object will be passed through to all invoked encoding functions.
 * @param {object} [params.mod=false] - Flag indicating if tuples in the input
 *   mod set that are unmodified by encoders should be included in the output.
 * @param {object} param.encoders - The encoding functions
 * @param {function(object, object): boolean} [param.encoders.update] - Update encoding set
 * @param {function(object, object): boolean} [param.encoders.enter] - Enter encoding set
 * @param {function(object, object): boolean} [param.encoders.exit] - Exit encoding set
 */
function Encode$1(params) {
  Transform.call(this, null, params);
}
inherits(Encode$1, Transform, {
  transform(_, pulse) {
    var out = pulse.fork(pulse.ADD_REM),
      fmod = _.mod || false,
      encoders = _.encoders,
      encode = pulse.encode;

    // if an array, the encode directive includes additional sets
    // that must be defined in order for the primary set to be invoked
    // e.g., only run the update set if the hover set is defined
    if (isArray(encode)) {
      if (out.changed() || encode.every(e => encoders[e])) {
        encode = encode[0];
        out.encode = null; // consume targeted encode directive
      } else {
        return pulse.StopPropagation;
      }
    }

    // marshall encoder functions
    var reenter = encode === 'enter',
      update = encoders.update || falsy,
      enter = encoders.enter || falsy,
      exit = encoders.exit || falsy,
      set = (encode && !reenter ? encoders[encode] : update) || falsy;
    if (pulse.changed(pulse.ADD)) {
      pulse.visit(pulse.ADD, t => {
        enter(t, _);
        update(t, _);
      });
      out.modifies(enter.output);
      out.modifies(update.output);
      if (set !== falsy && set !== update) {
        pulse.visit(pulse.ADD, t => {
          set(t, _);
        });
        out.modifies(set.output);
      }
    }
    if (pulse.changed(pulse.REM) && exit !== falsy) {
      pulse.visit(pulse.REM, t => {
        exit(t, _);
      });
      out.modifies(exit.output);
    }
    if (reenter || set !== falsy) {
      const flag = pulse.MOD | (_.modified() ? pulse.REFLOW : 0);
      if (reenter) {
        pulse.visit(flag, t => {
          const mod = enter(t, _) || fmod;
          if (set(t, _) || mod) out.mod.push(t);
        });
        if (out.mod.length) out.modifies(enter.output);
      } else {
        pulse.visit(flag, t => {
          if (set(t, _) || fmod) out.mod.push(t);
        });
      }
      if (out.mod.length) out.modifies(set.output);
    }
    return out.changed() ? out : pulse.StopPropagation;
  }
});

/**
 * Generates legend entries for visualizing a scale.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Scale} params.scale - The scale to generate items for.
 * @param {*} [params.count=5] - The approximate number of items, or
 *   desired tick interval, to use.
 * @param {*} [params.limit] - The maximum number of entries to
 *   include in a symbol legend.
 * @param {Array<*>} [params.values] - The exact tick values to use.
 *   These must be legal domain values for the provided scale.
 *   If provided, the count argument is ignored.
 * @param {string} [params.formatSpecifier] - A format specifier
 *   to use in conjunction with scale.tickFormat. Legal values are
 *   any valid D3 format specifier string.
 * @param {function(*):string} [params.format] - The format function to use.
 *   If provided, the formatSpecifier argument is ignored.
 */
function LegendEntries$1(params) {
  Transform.call(this, [], params);
}
inherits(LegendEntries$1, Transform, {
  transform(_, pulse) {
    if (this.value != null && !_.modified()) {
      return pulse.StopPropagation;
    }
    var locale = pulse.dataflow.locale(),
      out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      items = this.value,
      type = _.type || SymbolLegend,
      scale = _.scale,
      limit = +_.limit,
      count = tickCount(scale, _.count == null ? 5 : _.count, _.minstep),
      lskip = !!_.values || type === SymbolLegend,
      format = _.format || labelFormat(locale, scale, count, type, _.formatSpecifier, _.formatType, lskip),
      values = _.values || labelValues(scale, count),
      domain,
      fraction,
      size,
      offset,
      ellipsis;
    if (items) out.rem = items;
    if (type === SymbolLegend) {
      if (limit && values.length > limit) {
        pulse.dataflow.warn('Symbol legend count exceeds limit, filtering items.');
        items = values.slice(0, limit - 1);
        ellipsis = true;
      } else {
        items = values;
      }
      if (isFunction(size = _.size)) {
        // if first value maps to size zero, remove from list (vega#717)
        if (!_.values && scale(items[0]) === 0) {
          items = items.slice(1);
        }
        // compute size offset for legend entries
        offset = items.reduce((max, value) => Math.max(max, size(value, _)), 0);
      } else {
        size = constant$1(offset = size || 8);
      }
      items = items.map((value, index) => ingest$1({
        index: index,
        label: format(value, index, items),
        value: value,
        offset: offset,
        size: size(value, _)
      }));
      if (ellipsis) {
        ellipsis = values[items.length];
        items.push(ingest$1({
          index: items.length,
          label: `\u2026${values.length - items.length} entries`,
          value: ellipsis,
          offset: offset,
          size: size(ellipsis, _)
        }));
      }
    } else if (type === GradientLegend) {
      domain = scale.domain(), fraction = scaleFraction(scale, domain[0], peek$1(domain));

      // if automatic label generation produces 2 or fewer values,
      // use the domain end points instead (fixes vega/vega#1364)
      if (values.length < 3 && !_.values && domain[0] !== peek$1(domain)) {
        values = [domain[0], peek$1(domain)];
      }
      items = values.map((value, index) => ingest$1({
        index: index,
        label: format(value, index, values),
        value: value,
        perc: fraction(value)
      }));
    } else {
      size = values.length - 1;
      fraction = labelFraction(scale);
      items = values.map((value, index) => ingest$1({
        index: index,
        label: format(value, index, values),
        value: value,
        perc: index ? fraction(value) : 0,
        perc2: index === size ? 1 : fraction(values[index + 1])
      }));
    }
    out.source = items;
    out.add = items;
    this.value = items;
    return out;
  }
});

const sourceX = t => t.source.x;
const sourceY = t => t.source.y;
const targetX = t => t.target.x;
const targetY = t => t.target.y;

/**
 * Layout paths linking source and target elements.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function LinkPath(params) {
  Transform.call(this, {}, params);
}
LinkPath.Definition = {
  'type': 'LinkPath',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'sourceX',
    'type': 'field',
    'default': 'source.x'
  }, {
    'name': 'sourceY',
    'type': 'field',
    'default': 'source.y'
  }, {
    'name': 'targetX',
    'type': 'field',
    'default': 'target.x'
  }, {
    'name': 'targetY',
    'type': 'field',
    'default': 'target.y'
  }, {
    'name': 'orient',
    'type': 'enum',
    'default': 'vertical',
    'values': ['horizontal', 'vertical', 'radial']
  }, {
    'name': 'shape',
    'type': 'enum',
    'default': 'line',
    'values': ['line', 'arc', 'curve', 'diagonal', 'orthogonal']
  }, {
    'name': 'require',
    'type': 'signal'
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'path'
  }]
};
inherits(LinkPath, Transform, {
  transform(_, pulse) {
    var sx = _.sourceX || sourceX,
      sy = _.sourceY || sourceY,
      tx = _.targetX || targetX,
      ty = _.targetY || targetY,
      as = _.as || 'path',
      orient = _.orient || 'vertical',
      shape = _.shape || 'line',
      path = Paths.get(shape + '-' + orient) || Paths.get(shape);
    if (!path) {
      error$1('LinkPath unsupported type: ' + _.shape + (_.orient ? '-' + _.orient : ''));
    }
    pulse.visit(pulse.SOURCE, t => {
      t[as] = path(sx(t), sy(t), tx(t), ty(t));
    });
    return pulse.reflow(_.modified()).modifies(as);
  }
});
const line$1 = (sx, sy, tx, ty) => 'M' + sx + ',' + sy + 'L' + tx + ',' + ty;
const lineR = (sa, sr, ta, tr) => line$1(sr * Math.cos(sa), sr * Math.sin(sa), tr * Math.cos(ta), tr * Math.sin(ta));
const arc$1 = (sx, sy, tx, ty) => {
  var dx = tx - sx,
    dy = ty - sy,
    rr = Math.hypot(dx, dy) / 2,
    ra = 180 * Math.atan2(dy, dx) / Math.PI;
  return 'M' + sx + ',' + sy + 'A' + rr + ',' + rr + ' ' + ra + ' 0 1' + ' ' + tx + ',' + ty;
};
const arcR = (sa, sr, ta, tr) => arc$1(sr * Math.cos(sa), sr * Math.sin(sa), tr * Math.cos(ta), tr * Math.sin(ta));
const curve = (sx, sy, tx, ty) => {
  const dx = tx - sx,
    dy = ty - sy,
    ix = 0.2 * (dx + dy),
    iy = 0.2 * (dy - dx);
  return 'M' + sx + ',' + sy + 'C' + (sx + ix) + ',' + (sy + iy) + ' ' + (tx + iy) + ',' + (ty - ix) + ' ' + tx + ',' + ty;
};
const curveR = (sa, sr, ta, tr) => curve(sr * Math.cos(sa), sr * Math.sin(sa), tr * Math.cos(ta), tr * Math.sin(ta));
const orthoX = (sx, sy, tx, ty) => 'M' + sx + ',' + sy + 'V' + ty + 'H' + tx;
const orthoY = (sx, sy, tx, ty) => 'M' + sx + ',' + sy + 'H' + tx + 'V' + ty;
const orthoR = (sa, sr, ta, tr) => {
  const sc = Math.cos(sa),
    ss = Math.sin(sa),
    tc = Math.cos(ta),
    ts = Math.sin(ta),
    sf = Math.abs(ta - sa) > Math.PI ? ta <= sa : ta > sa;
  return 'M' + sr * sc + ',' + sr * ss + 'A' + sr + ',' + sr + ' 0 0,' + (sf ? 1 : 0) + ' ' + sr * tc + ',' + sr * ts + 'L' + tr * tc + ',' + tr * ts;
};
const diagonalX = (sx, sy, tx, ty) => {
  const m = (sx + tx) / 2;
  return 'M' + sx + ',' + sy + 'C' + m + ',' + sy + ' ' + m + ',' + ty + ' ' + tx + ',' + ty;
};
const diagonalY = (sx, sy, tx, ty) => {
  const m = (sy + ty) / 2;
  return 'M' + sx + ',' + sy + 'C' + sx + ',' + m + ' ' + tx + ',' + m + ' ' + tx + ',' + ty;
};
const diagonalR = (sa, sr, ta, tr) => {
  const sc = Math.cos(sa),
    ss = Math.sin(sa),
    tc = Math.cos(ta),
    ts = Math.sin(ta),
    mr = (sr + tr) / 2;
  return 'M' + sr * sc + ',' + sr * ss + 'C' + mr * sc + ',' + mr * ss + ' ' + mr * tc + ',' + mr * ts + ' ' + tr * tc + ',' + tr * ts;
};
const Paths = fastmap({
  'line': line$1,
  'line-radial': lineR,
  'arc': arc$1,
  'arc-radial': arcR,
  'curve': curve,
  'curve-radial': curveR,
  'orthogonal-horizontal': orthoX,
  'orthogonal-vertical': orthoY,
  'orthogonal-radial': orthoR,
  'diagonal-horizontal': diagonalX,
  'diagonal-vertical': diagonalY,
  'diagonal-radial': diagonalR
});

/**
 * Pie and donut chart layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to size pie segments.
 * @param {number} [params.startAngle=0] - The start angle (in radians) of the layout.
 * @param {number} [params.endAngle=2π] - The end angle (in radians) of the layout.
 * @param {boolean} [params.sort] - Boolean flag for sorting sectors by value.
 */
function Pie(params) {
  Transform.call(this, null, params);
}
Pie.Definition = {
  'type': 'Pie',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'startAngle',
    'type': 'number',
    'default': 0
  }, {
    'name': 'endAngle',
    'type': 'number',
    'default': 6.283185307179586
  }, {
    'name': 'sort',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': ['startAngle', 'endAngle']
  }]
};
inherits(Pie, Transform, {
  transform(_, pulse) {
    var as = _.as || ['startAngle', 'endAngle'],
      startAngle = as[0],
      endAngle = as[1],
      field = _.field || one$1,
      start = _.startAngle || 0,
      stop = _.endAngle != null ? _.endAngle : 2 * Math.PI,
      data = pulse.source,
      values = data.map(field),
      n = values.length,
      a = start,
      k = (stop - start) / sum(values),
      index = range$4(n),
      i,
      t,
      v;
    if (_.sort) {
      index.sort((a, b) => values[a] - values[b]);
    }
    for (i = 0; i < n; ++i) {
      v = values[index[i]];
      t = data[index[i]];
      t[startAngle] = a;
      t[endAngle] = a += v * k;
    }
    this.value = values;
    return pulse.reflow(_.modified()).modifies(as);
  }
});

const DEFAULT_COUNT = 5;
function includeZero(scale) {
  const type = scale.type;
  return !scale.bins && (type === Linear || type === Pow || type === Sqrt);
}
function includePad(type) {
  return isContinuous$1(type) && type !== Sequential;
}
const SKIP$1 = toSet(['set', 'modified', 'clear', 'type', 'scheme', 'schemeExtent', 'schemeCount', 'domain', 'domainMin', 'domainMid', 'domainMax', 'domainRaw', 'domainImplicit', 'nice', 'zero', 'bins', 'range', 'rangeStep', 'round', 'reverse', 'interpolate', 'interpolateGamma']);

/**
 * Maintains a scale function mapping data values to visual channels.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function Scale$1(params) {
  Transform.call(this, null, params);
  this.modified(true); // always treat as modified
}
inherits(Scale$1, Transform, {
  transform(_, pulse) {
    var df = pulse.dataflow,
      scale$1 = this.value,
      key = scaleKey(_);
    if (!scale$1 || key !== scale$1.type) {
      this.value = scale$1 = scale$4(key)();
    }
    for (key in _) if (!SKIP$1[key]) {
      // padding is a scale property for band/point but not others
      if (key === 'padding' && includePad(scale$1.type)) continue;
      // invoke scale property setter, raise warning if not found
      isFunction(scale$1[key]) ? scale$1[key](_[key]) : df.warn('Unsupported scale property: ' + key);
    }
    configureRange(scale$1, _, configureBins(scale$1, _, configureDomain(scale$1, _, df)));
    return pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
  }
});
function scaleKey(_) {
  var t = _.type,
    d = '',
    n;

  // backwards compatibility pre Vega 5.
  if (t === Sequential) return Sequential + '-' + Linear;
  if (isContinuousColor(_)) {
    n = _.rawDomain ? _.rawDomain.length : _.domain ? _.domain.length + +(_.domainMid != null) : 0;
    d = n === 2 ? Sequential + '-' : n === 3 ? Diverging + '-' : '';
  }
  return (d + t || Linear).toLowerCase();
}
function isContinuousColor(_) {
  const t = _.type;
  return isContinuous$1(t) && t !== Time && t !== UTC && (_.scheme || _.range && _.range.length && _.range.every(isString));
}
function configureDomain(scale, _, df) {
  // check raw domain, if provided use that and exit early
  const raw = rawDomain(scale, _.domainRaw, df);
  if (raw > -1) return raw;
  var domain = _.domain,
    type = scale.type,
    zero = _.zero || _.zero === undefined && includeZero(scale),
    n,
    mid;
  if (!domain) return 0;

  // adjust domain based on zero, min, max settings
  if (zero || _.domainMin != null || _.domainMax != null || _.domainMid != null) {
    n = (domain = domain.slice()).length - 1 || 1;
    if (zero) {
      if (domain[0] > 0) domain[0] = 0;
      if (domain[n] < 0) domain[n] = 0;
    }
    if (_.domainMin != null) domain[0] = _.domainMin;
    if (_.domainMax != null) domain[n] = _.domainMax;
    if (_.domainMid != null) {
      mid = _.domainMid;
      const i = mid > domain[n] ? n + 1 : mid < domain[0] ? 0 : n;
      if (i !== n) df.warn('Scale domainMid exceeds domain min or max.', mid);
      domain.splice(i, 0, mid);
    }
  }

  // adjust continuous domain for minimum pixel padding
  if (includePad(type) && _.padding && domain[0] !== peek$1(domain)) {
    domain = padDomain(type, domain, _.range, _.padding, _.exponent, _.constant);
  }

  // set the scale domain
  scale.domain(domainCheck(type, domain, df));

  // if ordinal scale domain is defined, prevent implicit
  // domain construction as side-effect of scale lookup
  if (type === Ordinal) {
    scale.unknown(_.domainImplicit ? implicit : undefined);
  }

  // perform 'nice' adjustment as requested
  if (_.nice && scale.nice) {
    scale.nice(_.nice !== true && tickCount(scale, _.nice) || null);
  }

  // return the cardinality of the domain
  return domain.length;
}
function rawDomain(scale, raw, df) {
  if (raw) {
    scale.domain(domainCheck(scale.type, raw, df));
    return raw.length;
  } else {
    return -1;
  }
}
function padDomain(type, domain, range, pad, exponent, constant) {
  var span = Math.abs(peek$1(range) - range[0]),
    frac = span / (span - 2 * pad),
    d = type === Log ? zoomLog(domain, null, frac) : type === Sqrt ? zoomPow(domain, null, frac, 0.5) : type === Pow ? zoomPow(domain, null, frac, exponent || 1) : type === Symlog ? zoomSymlog(domain, null, frac, constant || 1) : zoomLinear(domain, null, frac);
  domain = domain.slice();
  domain[0] = d[0];
  domain[domain.length - 1] = d[1];
  return domain;
}
function domainCheck(type, domain, df) {
  if (isLogarithmic(type)) {
    // sum signs of domain values
    // if all pos or all neg, abs(sum) === domain.length
    var s = Math.abs(domain.reduce((s, v) => s + (v < 0 ? -1 : v > 0 ? 1 : 0), 0));
    if (s !== domain.length) {
      df.warn('Log scale domain includes zero: ' + $(domain));
    }
  }
  return domain;
}
function configureBins(scale, _, count) {
  let bins = _.bins;
  if (bins && !isArray(bins)) {
    // generate bin boundary array
    const domain = scale.domain(),
      lo = domain[0],
      hi = peek$1(domain),
      step = bins.step;
    let start = bins.start == null ? lo : bins.start,
      stop = bins.stop == null ? hi : bins.stop;
    if (!step) error$1('Scale bins parameter missing step property.');
    if (start < lo) start = step * Math.ceil(lo / step);
    if (stop > hi) stop = step * Math.floor(hi / step);
    bins = range$4(start, stop + step / 2, step);
  }
  if (bins) {
    // assign bin boundaries to scale instance
    scale.bins = bins;
  } else if (scale.bins) {
    // no current bins, remove bins if previously set
    delete scale.bins;
  }

  // special handling for bin-ordinal scales
  if (scale.type === BinOrdinal) {
    if (!bins) {
      // the domain specifies the bins
      scale.bins = scale.domain();
    } else if (!_.domain && !_.domainRaw) {
      // the bins specify the domain
      scale.domain(bins);
      count = bins.length;
    }
  }

  // return domain cardinality
  return count;
}
function configureRange(scale, _, count) {
  var type = scale.type,
    round = _.round || false,
    range = _.range;

  // if range step specified, calculate full range extent
  if (_.rangeStep != null) {
    range = configureRangeStep(type, _, count);
  }

  // else if a range scheme is defined, use that
  else if (_.scheme) {
    range = configureScheme(type, _, count);
    if (isFunction(range)) {
      if (scale.interpolator) {
        return scale.interpolator(range);
      } else {
        error$1(`Scale type ${type} does not support interpolating color schemes.`);
      }
    }
  }

  // given a range array for an interpolating scale, convert to interpolator
  if (range && isInterpolating(type)) {
    return scale.interpolator(interpolateColors(flip(range, _.reverse), _.interpolate, _.interpolateGamma));
  }

  // configure rounding / interpolation
  if (range && _.interpolate && scale.interpolate) {
    scale.interpolate(interpolate$1(_.interpolate, _.interpolateGamma));
  } else if (isFunction(scale.round)) {
    scale.round(round);
  } else if (isFunction(scale.rangeRound)) {
    scale.interpolate(round ? interpolateRound : interpolate$1$1);
  }
  if (range) scale.range(flip(range, _.reverse));
}
function configureRangeStep(type, _, count) {
  if (type !== Band && type !== Point) {
    error$1('Only band and point scales support rangeStep.');
  }

  // calculate full range based on requested step size and padding
  var outer = (_.paddingOuter != null ? _.paddingOuter : _.padding) || 0,
    inner = type === Point ? 1 : (_.paddingInner != null ? _.paddingInner : _.padding) || 0;
  return [0, _.rangeStep * bandSpace(count, inner, outer)];
}
function configureScheme(type, _, count) {
  var extent = _.schemeExtent,
    name,
    scheme$1$1;
  if (isArray(_.scheme)) {
    scheme$1$1 = interpolateColors(_.scheme, _.interpolate, _.interpolateGamma);
  } else {
    name = _.scheme.toLowerCase();
    scheme$1$1 = scheme$1(name);
    if (!scheme$1$1) error$1(`Unrecognized scheme name: ${_.scheme}`);
  }

  // determine size for potential discrete range
  count = type === Threshold ? count + 1 : type === BinOrdinal ? count - 1 : type === Quantile || type === Quantize ? +_.schemeCount || DEFAULT_COUNT : count;

  // adjust and/or quantize scheme as appropriate
  return isInterpolating(type) ? adjustScheme(scheme$1$1, extent, _.reverse) : isFunction(scheme$1$1) ? quantizeInterpolator(adjustScheme(scheme$1$1, extent), count) : type === Ordinal ? scheme$1$1 : scheme$1$1.slice(0, count);
}
function adjustScheme(scheme, extent, reverse) {
  return isFunction(scheme) && (extent || reverse) ? interpolateRange$1(scheme, flip(extent || [0, 1], reverse)) : scheme;
}
function flip(array, reverse) {
  return reverse ? array.slice().reverse() : array;
}

/**
 * Sorts scenegraph items in the pulse source array.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(*,*): number} [params.sort] - A comparator
 *   function for sorting tuples.
 */
function SortItems$1(params) {
  Transform.call(this, null, params);
}
inherits(SortItems$1, Transform, {
  transform(_, pulse) {
    const mod = _.modified('sort') || pulse.changed(pulse.ADD) || pulse.modified(_.sort.fields) || pulse.modified('datum');
    if (mod) pulse.source.sort(stableCompare(_.sort));
    this.modified(mod);
    return pulse;
  }
});

const Zero = 'zero',
  Center$1 = 'center',
  Normalize = 'normalize',
  DefOutput = ['y0', 'y1'];

/**
 * Stack layout for visualization elements.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to stack.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors to groupby.
 * @param {function(object,object): number} [params.sort] - A comparator for stack sorting.
 * @param {string} [offset='zero'] - Stack baseline offset. One of 'zero', 'center', 'normalize'.
 */
function Stack(params) {
  Transform.call(this, null, params);
}
Stack.Definition = {
  'type': 'Stack',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'sort',
    'type': 'compare'
  }, {
    'name': 'offset',
    'type': 'enum',
    'default': Zero,
    'values': [Zero, Center$1, Normalize]
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': DefOutput
  }]
};
inherits(Stack, Transform, {
  transform(_, pulse) {
    var as = _.as || DefOutput,
      y0 = as[0],
      y1 = as[1],
      sort = stableCompare(_.sort),
      field = _.field || one$1,
      stack = _.offset === Center$1 ? stackCenter : _.offset === Normalize ? stackNormalize : stackZero,
      groups,
      i,
      n,
      max;

    // partition, sum, and sort the stack groups
    groups = partition$2(pulse.source, _.groupby, sort, field);

    // compute stack layouts per group
    for (i = 0, n = groups.length, max = groups.max; i < n; ++i) {
      stack(groups[i], max, field, y0, y1);
    }
    return pulse.reflow(_.modified()).modifies(as);
  }
});
function stackCenter(group, max, field, y0, y1) {
  var last = (max - group.sum) / 2,
    m = group.length,
    j = 0,
    t;
  for (; j < m; ++j) {
    t = group[j];
    t[y0] = last;
    t[y1] = last += Math.abs(field(t));
  }
}
function stackNormalize(group, max, field, y0, y1) {
  var scale = 1 / group.sum,
    last = 0,
    m = group.length,
    j = 0,
    v = 0,
    t;
  for (; j < m; ++j) {
    t = group[j];
    t[y0] = last;
    t[y1] = last = scale * (v += Math.abs(field(t)));
  }
}
function stackZero(group, max, field, y0, y1) {
  var lastPos = 0,
    lastNeg = 0,
    m = group.length,
    j = 0,
    v,
    t;
  for (; j < m; ++j) {
    t = group[j];
    v = +field(t);
    if (v < 0) {
      t[y0] = lastNeg;
      t[y1] = lastNeg += v;
    } else {
      t[y0] = lastPos;
      t[y1] = lastPos += v;
    }
  }
}
function partition$2(data, groupby, sort, field) {
  var groups = [],
    get = f => f(t),
    map,
    i,
    n,
    m,
    t,
    k,
    g,
    s,
    max;

  // partition data points into stack groups
  if (groupby == null) {
    groups.push(data.slice());
  } else {
    for (map = {}, i = 0, n = data.length; i < n; ++i) {
      t = data[i];
      k = groupby.map(get);
      g = map[k];
      if (!g) {
        map[k] = g = [];
        groups.push(g);
      }
      g.push(t);
    }
  }

  // compute sums of groups, sort groups as needed
  for (k = 0, max = 0, m = groups.length; k < m; ++k) {
    g = groups[k];
    for (i = 0, s = 0, n = g.length; i < n; ++i) {
      s += Math.abs(field(g[i]));
    }
    g.sum = s;
    if (s > max) max = s;
    if (sort) g.sort(sort);
  }
  groups.max = max;
  return groups;
}

const encode$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  axisticks: AxisTicks$1,
  datajoin: DataJoin$1,
  encode: Encode$1,
  legendentries: LegendEntries$1,
  linkpath: LinkPath,
  pie: Pie,
  scale: Scale$1,
  sortitems: SortItems$1,
  stack: Stack
}, Symbol.toStringTag, { value: 'Module' }));

const defaultPath = geoPath();
const projectionProperties = [
// standard properties in d3-geo
'clipAngle', 'clipExtent', 'scale', 'translate', 'center', 'rotate', 'parallels', 'precision', 'reflectX', 'reflectY',
// extended properties in d3-geo-projections
'coefficient', 'distance', 'fraction', 'lobes', 'parallel', 'radius', 'ratio', 'spacing', 'tilt'];

/**
 * Augment projections with their type and a copy method.
 */
function create$1(type, constructor) {
  return function projection() {
    const p = constructor();
    p.type = type;
    p.path = geoPath().projection(p);
    p.copy = p.copy || function () {
      const c = projection();
      projectionProperties.forEach(prop => {
        if (p[prop]) c[prop](p[prop]());
      });
      c.path.pointRadius(p.path.pointRadius());
      return c;
    };
    return registerScale(p);
  };
}
function projection(type, proj) {
  if (!type || typeof type !== 'string') {
    throw new Error('Projection type must be a name string.');
  }
  type = type.toLowerCase();
  if (arguments.length > 1) {
    projections[type] = create$1(type, proj);
    return this;
  } else {
    return projections[type] || null;
  }
}
function getProjectionPath(proj) {
  return proj && proj.path || defaultPath;
}
const projections = {
  // base d3-geo projection types
  albers: geoAlbers,
  albersusa: geoAlbersUsa,
  azimuthalequalarea: geoAzimuthalEqualArea,
  azimuthalequidistant: geoAzimuthalEquidistant,
  conicconformal: geoConicConformal,
  conicequalarea: geoConicEqualArea,
  conicequidistant: geoConicEquidistant,
  equalEarth: geoEqualEarth,
  equirectangular: geoEquirectangular,
  gnomonic: geoGnomonic,
  identity: geoIdentity,
  mercator: geoMercator,
  mollweide: geoMollweide,
  naturalEarth1: geoNaturalEarth1,
  orthographic: geoOrthographic,
  stereographic: geoStereographic,
  transversemercator: geoTransverseMercator
};
for (const key in projections) {
  projection(key, projections[key]);
}

function noop() {}
const cases = [[], [[[1.0, 1.5], [0.5, 1.0]]], [[[1.5, 1.0], [1.0, 1.5]]], [[[1.5, 1.0], [0.5, 1.0]]], [[[1.0, 0.5], [1.5, 1.0]]], [[[1.0, 1.5], [0.5, 1.0]], [[1.0, 0.5], [1.5, 1.0]]], [[[1.0, 0.5], [1.0, 1.5]]], [[[1.0, 0.5], [0.5, 1.0]]], [[[0.5, 1.0], [1.0, 0.5]]], [[[1.0, 1.5], [1.0, 0.5]]], [[[0.5, 1.0], [1.0, 0.5]], [[1.5, 1.0], [1.0, 1.5]]], [[[1.5, 1.0], [1.0, 0.5]]], [[[0.5, 1.0], [1.5, 1.0]]], [[[1.0, 1.5], [1.5, 1.0]]], [[[0.5, 1.0], [1.0, 1.5]]], []];

// Implementation adapted from d3/d3-contour. Thanks!
function contours () {
  var dx = 1,
    dy = 1,
    smooth = smoothLinear;
  function contours(values, tz) {
    return tz.map(value => contour(values, value));
  }

  // Accumulate, smooth contour rings, assign holes to exterior rings.
  // Based on https://github.com/mbostock/shapefile/blob/v0.6.2/shp/polygon.js
  function contour(values, value) {
    var polygons = [],
      holes = [];
    isorings(values, value, ring => {
      smooth(ring, values, value);
      if (area$1(ring) > 0) polygons.push([ring]);else holes.push(ring);
    });
    holes.forEach(hole => {
      for (var i = 0, n = polygons.length, polygon; i < n; ++i) {
        if (contains$1((polygon = polygons[i])[0], hole) !== -1) {
          polygon.push(hole);
          return;
        }
      }
    });
    return {
      type: 'MultiPolygon',
      value: value,
      coordinates: polygons
    };
  }

  // Marching squares with isolines stitched into rings.
  // Based on https://github.com/topojson/topojson-client/blob/v3.0.0/src/stitch.js
  function isorings(values, value, callback) {
    var fragmentByStart = [],
      fragmentByEnd = [],
      x,
      y,
      t0,
      t1,
      t2,
      t3;

    // Special case for the first row (y = -1, t2 = t3 = 0).
    x = y = -1;
    t1 = values[0] >= value;
    cases[t1 << 1].forEach(stitch);
    while (++x < dx - 1) {
      t0 = t1, t1 = values[x + 1] >= value;
      cases[t0 | t1 << 1].forEach(stitch);
    }
    cases[t1 << 0].forEach(stitch);

    // General case for the intermediate rows.
    while (++y < dy - 1) {
      x = -1;
      t1 = values[y * dx + dx] >= value;
      t2 = values[y * dx] >= value;
      cases[t1 << 1 | t2 << 2].forEach(stitch);
      while (++x < dx - 1) {
        t0 = t1, t1 = values[y * dx + dx + x + 1] >= value;
        t3 = t2, t2 = values[y * dx + x + 1] >= value;
        cases[t0 | t1 << 1 | t2 << 2 | t3 << 3].forEach(stitch);
      }
      cases[t1 | t2 << 3].forEach(stitch);
    }

    // Special case for the last row (y = dy - 1, t0 = t1 = 0).
    x = -1;
    t2 = values[y * dx] >= value;
    cases[t2 << 2].forEach(stitch);
    while (++x < dx - 1) {
      t3 = t2, t2 = values[y * dx + x + 1] >= value;
      cases[t2 << 2 | t3 << 3].forEach(stitch);
    }
    cases[t2 << 3].forEach(stitch);
    function stitch(line) {
      var start = [line[0][0] + x, line[0][1] + y],
        end = [line[1][0] + x, line[1][1] + y],
        startIndex = index(start),
        endIndex = index(end),
        f,
        g;
      if (f = fragmentByEnd[startIndex]) {
        if (g = fragmentByStart[endIndex]) {
          delete fragmentByEnd[f.end];
          delete fragmentByStart[g.start];
          if (f === g) {
            f.ring.push(end);
            callback(f.ring);
          } else {
            fragmentByStart[f.start] = fragmentByEnd[g.end] = {
              start: f.start,
              end: g.end,
              ring: f.ring.concat(g.ring)
            };
          }
        } else {
          delete fragmentByEnd[f.end];
          f.ring.push(end);
          fragmentByEnd[f.end = endIndex] = f;
        }
      } else if (f = fragmentByStart[endIndex]) {
        if (g = fragmentByEnd[startIndex]) {
          delete fragmentByStart[f.start];
          delete fragmentByEnd[g.end];
          if (f === g) {
            f.ring.push(end);
            callback(f.ring);
          } else {
            fragmentByStart[g.start] = fragmentByEnd[f.end] = {
              start: g.start,
              end: f.end,
              ring: g.ring.concat(f.ring)
            };
          }
        } else {
          delete fragmentByStart[f.start];
          f.ring.unshift(start);
          fragmentByStart[f.start = startIndex] = f;
        }
      } else {
        fragmentByStart[startIndex] = fragmentByEnd[endIndex] = {
          start: startIndex,
          end: endIndex,
          ring: [start, end]
        };
      }
    }
  }
  function index(point) {
    return point[0] * 2 + point[1] * (dx + 1) * 4;
  }
  function smoothLinear(ring, values, value) {
    ring.forEach(point => {
      var x = point[0],
        y = point[1],
        xt = x | 0,
        yt = y | 0,
        v0,
        v1 = values[yt * dx + xt];
      if (x > 0 && x < dx && xt === x) {
        v0 = values[yt * dx + xt - 1];
        point[0] = x + (value - v0) / (v1 - v0) - 0.5;
      }
      if (y > 0 && y < dy && yt === y) {
        v0 = values[(yt - 1) * dx + xt];
        point[1] = y + (value - v0) / (v1 - v0) - 0.5;
      }
    });
  }
  contours.contour = contour;
  contours.size = function (_) {
    if (!arguments.length) return [dx, dy];
    var _0 = Math.floor(_[0]),
      _1 = Math.floor(_[1]);
    if (!(_0 >= 0 && _1 >= 0)) error$1('invalid size');
    return dx = _0, dy = _1, contours;
  };
  contours.smooth = function (_) {
    return arguments.length ? (smooth = _ ? smoothLinear : noop, contours) : smooth === smoothLinear;
  };
  return contours;
}
function area$1(ring) {
  var i = 0,
    n = ring.length,
    area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1];
  while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1];
  return area;
}
function contains$1(ring, hole) {
  var i = -1,
    n = hole.length,
    c;
  while (++i < n) if (c = ringContains(ring, hole[i])) return c;
  return 0;
}
function ringContains(ring, point) {
  var x = point[0],
    y = point[1],
    contains = -1;
  for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) {
    var pi = ring[i],
      xi = pi[0],
      yi = pi[1],
      pj = ring[j],
      xj = pj[0],
      yj = pj[1];
    if (segmentContains(pi, pj, point)) return 0;
    if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) contains = -contains;
  }
  return contains;
}
function segmentContains(a, b, c) {
  var i;
  return collinear(a, b, c) && within(a[i = +(a[0] === b[0])], c[i], b[i]);
}
function collinear(a, b, c) {
  return (b[0] - a[0]) * (c[1] - a[1]) === (c[0] - a[0]) * (b[1] - a[1]);
}
function within(p, q, r) {
  return p <= q && q <= r || r <= q && q <= p;
}

function quantize (k, nice, zero) {
  return function (values) {
    var ex = extent(values),
      start = zero ? Math.min(ex[0], 0) : ex[0],
      stop = ex[1],
      span = stop - start,
      step = nice ? tickStep(start, stop, k) : span / (k + 1);
    return range$4(start + step, stop, step);
  };
}

/**
 * Generate isocontours (level sets) based on input raster grid data.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} [params.field] - The field with raster grid
 *   data. If unspecified, the tuple itself is interpreted as a raster grid.
 * @param {Array<number>} [params.thresholds] - Contour threshold array. If
 *   specified, the levels, nice, resolve, and zero parameters are ignored.
 * @param {number} [params.levels] - The desired number of contour levels.
 * @param {boolean} [params.nice] - Boolean flag indicating if the contour
 *   threshold values should be automatically aligned to "nice"
 *   human-friendly values. Setting this flag may cause the number of
 *   thresholds to deviate from the specified levels.
 * @param {string} [params.resolve] - The method for resolving thresholds
 *   across multiple input grids. If 'independent' (the default), threshold
 *   calculation will be performed separately for each grid. If 'shared', a
 *   single set of threshold values will be used for all input grids.
 * @param {boolean} [params.zero] - Boolean flag indicating if the contour
 *   threshold values should include zero.
 * @param {boolean} [params.smooth] - Boolean flag indicating if the contour
 *   polygons should be smoothed using linear interpolation. The default is
 *   true. The parameter is ignored when using density estimation.
 * @param {boolean} [params.scale] - Optional numerical value by which to
 *   scale the output isocontour coordinates. This parameter can be useful
 *   to scale the contours to match a desired output resolution.
 * @param {string} [params.as='contour'] - The output field in which to store
 *   the generated isocontour data (default 'contour').
 */
function Isocontour(params) {
  Transform.call(this, null, params);
}
Isocontour.Definition = {
  'type': 'Isocontour',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'thresholds',
    'type': 'number',
    'array': true
  }, {
    'name': 'levels',
    'type': 'number'
  }, {
    'name': 'nice',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'resolve',
    'type': 'enum',
    'values': ['shared', 'independent'],
    'default': 'independent'
  }, {
    'name': 'zero',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'smooth',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'scale',
    'type': 'number',
    'expr': true
  }, {
    'name': 'translate',
    'type': 'number',
    'array': true,
    'expr': true
  }, {
    'name': 'as',
    'type': 'string',
    'null': true,
    'default': 'contour'
  }]
};
inherits(Isocontour, Transform, {
  transform(_, pulse) {
    if (this.value && !pulse.changed() && !_.modified()) {
      return pulse.StopPropagation;
    }
    var out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      source = pulse.materialize(pulse.SOURCE).source,
      field = _.field || identity,
      contour = contours().smooth(_.smooth !== false),
      tz = _.thresholds || levels(source, field, _),
      as = _.as === null ? null : _.as || 'contour',
      values = [];
    source.forEach(t => {
      const grid = field(t);

      // generate contour paths in GeoJSON format
      const paths = contour.size([grid.width, grid.height])(grid.values, isArray(tz) ? tz : tz(grid.values));

      // adjust contour path coordinates as needed
      transformPaths(paths, grid, t, _);

      // ingest; copy source data properties to output
      paths.forEach(p => {
        values.push(rederive(t, ingest$1(as != null ? {
          [as]: p
        } : p)));
      });
    });
    if (this.value) out.rem = this.value;
    this.value = out.source = out.add = values;
    return out;
  }
});
function levels(values, f, _) {
  const q = quantize(_.levels || 10, _.nice, _.zero !== false);
  return _.resolve !== 'shared' ? q : q(values.map(t => max$1(f(t).values)));
}
function transformPaths(paths, grid, datum, _) {
  let s = _.scale || grid.scale,
    t = _.translate || grid.translate;
  if (isFunction(s)) s = s(datum, _);
  if (isFunction(t)) t = t(datum, _);
  if ((s === 1 || s == null) && !t) return;
  const sx = (isNumber$1(s) ? s : s[0]) || 1,
    sy = (isNumber$1(s) ? s : s[1]) || 1,
    tx = t && t[0] || 0,
    ty = t && t[1] || 0;
  paths.forEach(transform$1(grid, sx, sy, tx, ty));
}
function transform$1(grid, sx, sy, tx, ty) {
  const x1 = grid.x1 || 0,
    y1 = grid.y1 || 0,
    flip = sx * sy < 0;
  function transformPolygon(coordinates) {
    coordinates.forEach(transformRing);
  }
  function transformRing(coordinates) {
    if (flip) coordinates.reverse(); // maintain winding order
    coordinates.forEach(transformPoint);
  }
  function transformPoint(coordinates) {
    coordinates[0] = (coordinates[0] - x1) * sx + tx;
    coordinates[1] = (coordinates[1] - y1) * sy + ty;
  }
  return function (geometry) {
    geometry.coordinates.forEach(transformPolygon);
    return geometry;
  };
}

function radius(bw, data, f) {
  const v = bw >= 0 ? bw : estimateBandwidth(data, f);
  return Math.round((Math.sqrt(4 * v * v + 1) - 1) / 2);
}
function number$2(_) {
  return isFunction(_) ? _ : constant$1(+_);
}

// Implementation adapted from d3/d3-contour. Thanks!
function density2D () {
  var x = d => d[0],
    y = d => d[1],
    weight = one$1,
    bandwidth = [-1, -1],
    dx = 960,
    dy = 500,
    k = 2; // log2(cellSize)

  function density(data, counts) {
    const rx = radius(bandwidth[0], data, x) >> k,
      // blur x-radius
      ry = radius(bandwidth[1], data, y) >> k,
      // blur y-radius
      ox = rx ? rx + 2 : 0,
      // x-offset padding for blur
      oy = ry ? ry + 2 : 0,
      // y-offset padding for blur
      n = 2 * ox + (dx >> k),
      // grid width
      m = 2 * oy + (dy >> k),
      // grid height
      values0 = new Float32Array(n * m),
      values1 = new Float32Array(n * m);
    let values = values0;
    data.forEach(d => {
      const xi = ox + (+x(d) >> k),
        yi = oy + (+y(d) >> k);
      if (xi >= 0 && xi < n && yi >= 0 && yi < m) {
        values0[xi + yi * n] += +weight(d);
      }
    });
    if (rx > 0 && ry > 0) {
      blurX(n, m, values0, values1, rx);
      blurY(n, m, values1, values0, ry);
      blurX(n, m, values0, values1, rx);
      blurY(n, m, values1, values0, ry);
      blurX(n, m, values0, values1, rx);
      blurY(n, m, values1, values0, ry);
    } else if (rx > 0) {
      blurX(n, m, values0, values1, rx);
      blurX(n, m, values1, values0, rx);
      blurX(n, m, values0, values1, rx);
      values = values1;
    } else if (ry > 0) {
      blurY(n, m, values0, values1, ry);
      blurY(n, m, values1, values0, ry);
      blurY(n, m, values0, values1, ry);
      values = values1;
    }

    // scale density estimates
    // density in points per square pixel or probability density
    const s = counts ? Math.pow(2, -2 * k) : 1 / sum(values);
    for (let i = 0, sz = n * m; i < sz; ++i) values[i] *= s;
    return {
      values: values,
      scale: 1 << k,
      width: n,
      height: m,
      x1: ox,
      y1: oy,
      x2: ox + (dx >> k),
      y2: oy + (dy >> k)
    };
  }
  density.x = function (_) {
    return arguments.length ? (x = number$2(_), density) : x;
  };
  density.y = function (_) {
    return arguments.length ? (y = number$2(_), density) : y;
  };
  density.weight = function (_) {
    return arguments.length ? (weight = number$2(_), density) : weight;
  };
  density.size = function (_) {
    if (!arguments.length) return [dx, dy];
    var _0 = +_[0],
      _1 = +_[1];
    if (!(_0 >= 0 && _1 >= 0)) error$1('invalid size');
    return dx = _0, dy = _1, density;
  };
  density.cellSize = function (_) {
    if (!arguments.length) return 1 << k;
    if (!((_ = +_) >= 1)) error$1('invalid cell size');
    k = Math.floor(Math.log(_) / Math.LN2);
    return density;
  };
  density.bandwidth = function (_) {
    if (!arguments.length) return bandwidth;
    _ = array$2(_);
    if (_.length === 1) _ = [+_[0], +_[0]];
    if (_.length !== 2) error$1('invalid bandwidth');
    return bandwidth = _, density;
  };
  return density;
}
function blurX(n, m, source, target, r) {
  const w = (r << 1) + 1;
  for (let j = 0; j < m; ++j) {
    for (let i = 0, sr = 0; i < n + r; ++i) {
      if (i < n) {
        sr += source[i + j * n];
      }
      if (i >= r) {
        if (i >= w) {
          sr -= source[i - w + j * n];
        }
        target[i - r + j * n] = sr / Math.min(i + 1, n - 1 + w - i, w);
      }
    }
  }
}
function blurY(n, m, source, target, r) {
  const w = (r << 1) + 1;
  for (let i = 0; i < n; ++i) {
    for (let j = 0, sr = 0; j < m + r; ++j) {
      if (j < m) {
        sr += source[i + j * n];
      }
      if (j >= r) {
        if (j >= w) {
          sr -= source[i + (j - w) * n];
        }
        target[i + (j - r) * n] = sr / Math.min(j + 1, m - 1 + w - j, w);
      }
    }
  }
}

/**
 * Perform 2D kernel-density estimation of point data.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<number>} params.size - The [width, height] extent (in
 *   units of input pixels) over which to perform density estimation.
 * @param {function(object): number} params.x - The x-coordinate accessor.
 * @param {function(object): number} params.y - The y-coordinate accessor.
 * @param {function(object): number} [params.weight] - The weight accessor.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors
 *   to groupby.
 * @param {number} [params.cellSize] - Contour density calculation cell size.
 *   This parameter determines the level of spatial approximation. For example,
 *   the default value of 4 maps to 2x reductions in both x- and y- dimensions.
 *   A value of 1 will result in an output raster grid whose dimensions exactly
 *   matches the size parameter.
 * @param {Array<number>} [params.bandwidth] - The KDE kernel bandwidths,
 *   in pixels. The input can be a two-element array specifying separate
 *   x and y bandwidths, or a single-element array specifying both. If the
 *   bandwidth is unspecified or less than zero, the bandwidth will be
 *   automatically determined.
 * @param {boolean} [params.counts=false] - A boolean flag indicating if the
 *   output values should be probability estimates (false, default) or
 *   smoothed counts (true).
 * @param {string} [params.as='grid'] - The output field in which to store
 *   the generated raster grid (default 'grid').
 */
function KDE2D(params) {
  Transform.call(this, null, params);
}
KDE2D.Definition = {
  'type': 'KDE2D',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2,
    'required': true
  }, {
    'name': 'x',
    'type': 'field',
    'required': true
  }, {
    'name': 'y',
    'type': 'field',
    'required': true
  }, {
    'name': 'weight',
    'type': 'field'
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'cellSize',
    'type': 'number'
  }, {
    'name': 'bandwidth',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'counts',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'grid'
  }]
};
const PARAMS = ['x', 'y', 'weight', 'size', 'cellSize', 'bandwidth'];
function params(obj, _) {
  PARAMS.forEach(param => _[param] != null ? obj[param](_[param]) : 0);
  return obj;
}
inherits(KDE2D, Transform, {
  transform(_, pulse) {
    if (this.value && !pulse.changed() && !_.modified()) return pulse.StopPropagation;
    var out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      source = pulse.materialize(pulse.SOURCE).source,
      groups = partition$1(source, _.groupby),
      names = (_.groupby || []).map(accessorName),
      kde = params(density2D(), _),
      as = _.as || 'grid',
      values = [];
    function set(t, vals) {
      for (let i = 0; i < names.length; ++i) t[names[i]] = vals[i];
      return t;
    }

    // generate density raster grids
    values = groups.map(g => ingest$1(set({
      [as]: kde(g, _.counts)
    }, g.dims)));
    if (this.value) out.rem = this.value;
    this.value = out.source = out.add = values;
    return out;
  }
});
function partition$1(data, groupby) {
  var groups = [],
    get = f => f(t),
    map,
    i,
    n,
    t,
    k,
    g;

  // partition data points into groups
  if (groupby == null) {
    groups.push(data);
  } else {
    for (map = {}, i = 0, n = data.length; i < n; ++i) {
      t = data[i];
      k = groupby.map(get);
      g = map[k];
      if (!g) {
        map[k] = g = [];
        g.dims = k;
        groups.push(g);
      }
      g.push(t);
    }
  }
  return groups;
}

/**
 * Generate contours based on kernel-density estimation of point data.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<number>} params.size - The dimensions [width, height] over which to compute contours.
 *  If the values parameter is provided, this must be the dimensions of the input data.
 *  If density estimation is performed, this is the output view dimensions in pixels.
 * @param {Array<number>} [params.values] - An array of numeric values representing an
 *  width x height grid of values over which to compute contours. If unspecified, this
 *  transform will instead attempt to compute contours for the kernel density estimate
 *  using values drawn from data tuples in the input pulse.
 * @param {function(object): number} [params.x] - The pixel x-coordinate accessor for density estimation.
 * @param {function(object): number} [params.y] - The pixel y-coordinate accessor for density estimation.
 * @param {function(object): number} [params.weight] - The data point weight accessor for density estimation.
 * @param {number} [params.cellSize] - Contour density calculation cell size.
 * @param {number} [params.bandwidth] - Kernel density estimation bandwidth.
 * @param {Array<number>} [params.thresholds] - Contour threshold array. If
 *   this parameter is set, the count and nice parameters will be ignored.
 * @param {number} [params.count] - The desired number of contours.
 * @param {boolean} [params.nice] - Boolean flag indicating if the contour
 *   threshold values should be automatically aligned to "nice"
 *   human-friendly values. Setting this flag may cause the number of
 *   thresholds to deviate from the specified count.
 * @param {boolean} [params.smooth] - Boolean flag indicating if the contour
 *   polygons should be smoothed using linear interpolation. The default is
 *   true. The parameter is ignored when using density estimation.
 */
function Contour(params) {
  Transform.call(this, null, params);
}
Contour.Definition = {
  'type': 'Contour',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2,
    'required': true
  }, {
    'name': 'values',
    'type': 'number',
    'array': true
  }, {
    'name': 'x',
    'type': 'field'
  }, {
    'name': 'y',
    'type': 'field'
  }, {
    'name': 'weight',
    'type': 'field'
  }, {
    'name': 'cellSize',
    'type': 'number'
  }, {
    'name': 'bandwidth',
    'type': 'number'
  }, {
    'name': 'count',
    'type': 'number'
  }, {
    'name': 'nice',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'thresholds',
    'type': 'number',
    'array': true
  }, {
    'name': 'smooth',
    'type': 'boolean',
    'default': true
  }]
};
inherits(Contour, Transform, {
  transform(_, pulse) {
    if (this.value && !pulse.changed() && !_.modified()) {
      return pulse.StopPropagation;
    }
    var out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS),
      contour = contours().smooth(_.smooth !== false),
      values = _.values,
      thresh = _.thresholds || quantize(_.count || 10, _.nice, !!values),
      size = _.size,
      grid,
      post;
    if (!values) {
      values = pulse.materialize(pulse.SOURCE).source;
      grid = params(density2D(), _)(values, true);
      post = transform$1(grid, grid.scale || 1, grid.scale || 1, 0, 0);
      size = [grid.width, grid.height];
      values = grid.values;
    }
    thresh = isArray(thresh) ? thresh : thresh(values);
    values = contour.size(size)(values, thresh);
    if (post) values.forEach(post);
    if (this.value) out.rem = this.value;
    this.value = out.source = out.add = (values || []).map(ingest$1);
    return out;
  }
});

const Feature = 'Feature';
const FeatureCollection = 'FeatureCollection';
const MultiPoint = 'MultiPoint';

/**
 * Consolidate an array of [longitude, latitude] points or GeoJSON features
 * into a combined GeoJSON object. This transform is particularly useful for
 * combining geo data for a Projection's fit argument. The resulting GeoJSON
 * data is available as this transform's value. Input pulses are unchanged.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *>} [params.fields] - A two-element array
 *   of field accessors for the longitude and latitude values.
 * @param {function(object): *} params.geojson - A field accessor for
 *   retrieving GeoJSON feature data.
 */
function GeoJSON(params) {
  Transform.call(this, null, params);
}
GeoJSON.Definition = {
  'type': 'GeoJSON',
  'metadata': {},
  'params': [{
    'name': 'fields',
    'type': 'field',
    'array': true,
    'length': 2
  }, {
    'name': 'geojson',
    'type': 'field'
  }]
};
inherits(GeoJSON, Transform, {
  transform(_, pulse) {
    var features = this._features,
      points = this._points,
      fields = _.fields,
      lon = fields && fields[0],
      lat = fields && fields[1],
      geojson = _.geojson || !fields && identity,
      flag = pulse.ADD,
      mod;
    mod = _.modified() || pulse.changed(pulse.REM) || pulse.modified(accessorFields(geojson)) || lon && pulse.modified(accessorFields(lon)) || lat && pulse.modified(accessorFields(lat));
    if (!this.value || mod) {
      flag = pulse.SOURCE;
      this._features = features = [];
      this._points = points = [];
    }
    if (geojson) {
      pulse.visit(flag, t => features.push(geojson(t)));
    }
    if (lon && lat) {
      pulse.visit(flag, t => {
        var x = lon(t),
          y = lat(t);
        if (x != null && y != null && (x = +x) === x && (y = +y) === y) {
          points.push([x, y]);
        }
      });
      features = features.concat({
        type: Feature,
        geometry: {
          type: MultiPoint,
          coordinates: points
        }
      });
    }
    this.value = {
      type: FeatureCollection,
      features: features
    };
  }
});

/**
 * Map GeoJSON data to an SVG path string.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(number, number): *} params.projection - The cartographic
 *   projection to apply.
 * @param {function(object): *} [params.field] - The field with GeoJSON data,
 *   or null if the tuple itself is a GeoJSON feature.
 * @param {string} [params.as='path'] - The output field in which to store
 *   the generated path data (default 'path').
 */
function GeoPath(params) {
  Transform.call(this, null, params);
}
GeoPath.Definition = {
  'type': 'GeoPath',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'projection',
    'type': 'projection'
  }, {
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'pointRadius',
    'type': 'number',
    'expr': true
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'path'
  }]
};
inherits(GeoPath, Transform, {
  transform(_, pulse) {
    var out = pulse.fork(pulse.ALL),
      path = this.value,
      field = _.field || identity,
      as = _.as || 'path',
      flag = out.SOURCE;
    if (!path || _.modified()) {
      // parameters updated, reset and reflow
      this.value = path = getProjectionPath(_.projection);
      out.materialize().reflow();
    } else {
      flag = field === identity || pulse.modified(field.fields) ? out.ADD_MOD : out.ADD;
    }
    const prev = initPath(path, _.pointRadius);
    out.visit(flag, t => t[as] = path(field(t)));
    path.pointRadius(prev);
    return out.modifies(as);
  }
});
function initPath(path, pointRadius) {
  const prev = path.pointRadius();
  path.context(null);
  if (pointRadius != null) {
    path.pointRadius(pointRadius);
  }
  return prev;
}

/**
 * Geo-code a longitude/latitude point to an x/y coordinate.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(number, number): *} params.projection - The cartographic
 *   projection to apply.
 * @param {Array<function(object): *>} params.fields - A two-element array of
 *   field accessors for the longitude and latitude values.
 * @param {Array<string>} [params.as] - A two-element array of field names
 *   under which to store the result. Defaults to ['x','y'].
 */
function GeoPoint(params) {
  Transform.call(this, null, params);
}
GeoPoint.Definition = {
  'type': 'GeoPoint',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'projection',
    'type': 'projection',
    'required': true
  }, {
    'name': 'fields',
    'type': 'field',
    'array': true,
    'required': true,
    'length': 2
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 2,
    'default': ['x', 'y']
  }]
};
inherits(GeoPoint, Transform, {
  transform(_, pulse) {
    var proj = _.projection,
      lon = _.fields[0],
      lat = _.fields[1],
      as = _.as || ['x', 'y'],
      x = as[0],
      y = as[1],
      mod;
    function set(t) {
      const xy = proj([lon(t), lat(t)]);
      if (xy) {
        t[x] = xy[0];
        t[y] = xy[1];
      } else {
        t[x] = undefined;
        t[y] = undefined;
      }
    }
    if (_.modified()) {
      // parameters updated, reflow
      pulse = pulse.materialize().reflow(true).visit(pulse.SOURCE, set);
    } else {
      mod = pulse.modified(lon.fields) || pulse.modified(lat.fields);
      pulse.visit(mod ? pulse.ADD_MOD : pulse.ADD, set);
    }
    return pulse.modifies(as);
  }
});

/**
 * Annotate items with a geopath shape generator.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(number, number): *} params.projection - The cartographic
 *   projection to apply.
 * @param {function(object): *} [params.field] - The field with GeoJSON data,
 *   or null if the tuple itself is a GeoJSON feature.
 * @param {string} [params.as='shape'] - The output field in which to store
 *   the generated path data (default 'shape').
 */
function GeoShape(params) {
  Transform.call(this, null, params);
}
GeoShape.Definition = {
  'type': 'GeoShape',
  'metadata': {
    'modifies': true,
    'nomod': true
  },
  'params': [{
    'name': 'projection',
    'type': 'projection'
  }, {
    'name': 'field',
    'type': 'field',
    'default': 'datum'
  }, {
    'name': 'pointRadius',
    'type': 'number',
    'expr': true
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'shape'
  }]
};
inherits(GeoShape, Transform, {
  transform(_, pulse) {
    var out = pulse.fork(pulse.ALL),
      shape = this.value,
      as = _.as || 'shape',
      flag = out.ADD;
    if (!shape || _.modified()) {
      // parameters updated, reset and reflow
      this.value = shape = shapeGenerator(getProjectionPath(_.projection), _.field || field$1('datum'), _.pointRadius);
      out.materialize().reflow();
      flag = out.SOURCE;
    }
    out.visit(flag, t => t[as] = shape);
    return out.modifies(as);
  }
});
function shapeGenerator(path, field, pointRadius) {
  const shape = pointRadius == null ? _ => path(field(_)) : _ => {
    var prev = path.pointRadius(),
      value = path.pointRadius(pointRadius)(field(_));
    path.pointRadius(prev);
    return value;
  };
  shape.context = _ => {
    path.context(_);
    return shape;
  };
  return shape;
}

/**
 * GeoJSON feature generator for creating graticules.
 * @constructor
 */
function Graticule(params) {
  Transform.call(this, [], params);
  this.generator = graticule();
}
Graticule.Definition = {
  'type': 'Graticule',
  'metadata': {
    'changes': true,
    'generates': true
  },
  'params': [{
    'name': 'extent',
    'type': 'array',
    'array': true,
    'length': 2,
    'content': {
      'type': 'number',
      'array': true,
      'length': 2
    }
  }, {
    'name': 'extentMajor',
    'type': 'array',
    'array': true,
    'length': 2,
    'content': {
      'type': 'number',
      'array': true,
      'length': 2
    }
  }, {
    'name': 'extentMinor',
    'type': 'array',
    'array': true,
    'length': 2,
    'content': {
      'type': 'number',
      'array': true,
      'length': 2
    }
  }, {
    'name': 'step',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'stepMajor',
    'type': 'number',
    'array': true,
    'length': 2,
    'default': [90, 360]
  }, {
    'name': 'stepMinor',
    'type': 'number',
    'array': true,
    'length': 2,
    'default': [10, 10]
  }, {
    'name': 'precision',
    'type': 'number',
    'default': 2.5
  }]
};
inherits(Graticule, Transform, {
  transform(_, pulse) {
    var src = this.value,
      gen = this.generator,
      t;
    if (!src.length || _.modified()) {
      for (const prop in _) {
        if (isFunction(gen[prop])) {
          gen[prop](_[prop]);
        }
      }
    }
    t = gen();
    if (src.length) {
      pulse.mod.push(replace$1(src[0], t));
    } else {
      pulse.add.push(ingest$1(t));
    }
    src[0] = t;
    return pulse;
  }
});

/**
 * Render a heatmap image for input raster grid data.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} [params.field] - The field with raster grid
 *   data. If unspecified, the tuple itself is interpreted as a raster grid.
 * @param {string} [params.color] - A constant color value or function for
 *   individual pixel color. If a function, it will be invoked with an input
 *   object that includes $x, $y, $value, and $max fields for the grid.
 * @param {number} [params.opacity] - A constant opacity value or function for
 *   individual pixel opacity. If a function, it will be invoked with an input
 *   object that includes $x, $y, $value, and $max fields for the grid.
 * @param {string} [params.resolve] - The method for resolving maximum values
 *   across multiple input grids. If 'independent' (the default), maximum
 *   calculation will be performed separately for each grid. If 'shared',
 *   a single global maximum will be used for all input grids.
 * @param {string} [params.as='image'] - The output field in which to store
 *   the generated bitmap canvas images (default 'image').
 */
function Heatmap(params) {
  Transform.call(this, null, params);
}
Heatmap.Definition = {
  'type': 'heatmap',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'color',
    'type': 'string',
    'expr': true
  }, {
    'name': 'opacity',
    'type': 'number',
    'expr': true
  }, {
    'name': 'resolve',
    'type': 'enum',
    'values': ['shared', 'independent'],
    'default': 'independent'
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'image'
  }]
};
inherits(Heatmap, Transform, {
  transform(_, pulse) {
    if (!pulse.changed() && !_.modified()) {
      return pulse.StopPropagation;
    }
    var source = pulse.materialize(pulse.SOURCE).source,
      shared = _.resolve === 'shared',
      field = _.field || identity,
      opacity = opacity_(_.opacity, _),
      color = color_(_.color, _),
      as = _.as || 'image',
      obj = {
        $x: 0,
        $y: 0,
        $value: 0,
        $max: shared ? max$1(source.map(t => max$1(field(t).values))) : 0
      };
    source.forEach(t => {
      const v = field(t);

      // build proxy data object
      const o = extend({}, t, obj);
      // set maximum value if not globally shared
      if (!shared) o.$max = max$1(v.values || []);

      // generate canvas image
      // optimize color/opacity if not pixel-dependent
      t[as] = toCanvas(v, o, color.dep ? color : constant$1(color(o)), opacity.dep ? opacity : constant$1(opacity(o)));
    });
    return pulse.reflow(true).modifies(as);
  }
});

// get image color function
function color_(color, _) {
  let f;
  if (isFunction(color)) {
    f = obj => rgb(color(obj, _));
    f.dep = dependency(color);
  } else {
    // default to mid-grey
    f = constant$1(rgb(color || '#888'));
  }
  return f;
}

// get image opacity function
function opacity_(opacity, _) {
  let f;
  if (isFunction(opacity)) {
    f = obj => opacity(obj, _);
    f.dep = dependency(opacity);
  } else if (opacity) {
    f = constant$1(opacity);
  } else {
    // default to [0, max] opacity gradient
    f = obj => obj.$value / obj.$max || 0;
    f.dep = true;
  }
  return f;
}

// check if function depends on individual pixel data
function dependency(f) {
  if (!isFunction(f)) return false;
  const set = toSet(accessorFields(f));
  return set.$x || set.$y || set.$value || set.$max;
}

// render raster grid to canvas
function toCanvas(grid, obj, color, opacity) {
  const n = grid.width,
    m = grid.height,
    x1 = grid.x1 || 0,
    y1 = grid.y1 || 0,
    x2 = grid.x2 || n,
    y2 = grid.y2 || m,
    val = grid.values,
    value = val ? i => val[i] : zero$2,
    can = domCanvas(x2 - x1, y2 - y1),
    ctx = can.getContext('2d'),
    img = ctx.getImageData(0, 0, x2 - x1, y2 - y1),
    pix = img.data;
  for (let j = y1, k = 0; j < y2; ++j) {
    obj.$y = j - y1;
    for (let i = x1, r = j * n; i < x2; ++i, k += 4) {
      obj.$x = i - x1;
      obj.$value = value(i + r);
      const v = color(obj);
      pix[k + 0] = v.r;
      pix[k + 1] = v.g;
      pix[k + 2] = v.b;
      pix[k + 3] = ~~(255 * opacity(obj));
    }
  }
  ctx.putImageData(img, 0, 0);
  return can;
}

/**
 * Maintains a cartographic projection.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function Projection$1(params) {
  Transform.call(this, null, params);
  this.modified(true); // always treat as modified
}
inherits(Projection$1, Transform, {
  transform(_, pulse) {
    let proj = this.value;
    if (!proj || _.modified('type')) {
      this.value = proj = create(_.type);
      projectionProperties.forEach(prop => {
        if (_[prop] != null) set$1(proj, prop, _[prop]);
      });
    } else {
      projectionProperties.forEach(prop => {
        if (_.modified(prop)) set$1(proj, prop, _[prop]);
      });
    }
    if (_.pointRadius != null) proj.path.pointRadius(_.pointRadius);
    if (_.fit) fit(proj, _);
    return pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
  }
});
function fit(proj, _) {
  const data = collectGeoJSON(_.fit);
  _.extent ? proj.fitExtent(_.extent, data) : _.size ? proj.fitSize(_.size, data) : 0;
}
function create(type) {
  const constructor = projection((type || 'mercator').toLowerCase());
  if (!constructor) error$1('Unrecognized projection type: ' + type);
  return constructor();
}
function set$1(proj, key, value) {
  if (isFunction(proj[key])) proj[key](value);
}
function collectGeoJSON(data) {
  data = array$2(data);
  return data.length === 1 ? data[0] : {
    type: FeatureCollection,
    features: data.reduce((a, f) => a.concat(featurize(f)), [])
  };
}
function featurize(f) {
  return f.type === FeatureCollection ? f.features : array$2(f).filter(d => d != null).map(d => d.type === Feature ? d : {
    type: Feature,
    geometry: d
  });
}

const geo = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  contour: Contour,
  geojson: GeoJSON,
  geopath: GeoPath,
  geopoint: GeoPoint,
  geoshape: GeoShape,
  graticule: Graticule,
  heatmap: Heatmap,
  isocontour: Isocontour,
  kde2d: KDE2D,
  projection: Projection$1
}, Symbol.toStringTag, { value: 'Module' }));

const ForceMap = {
  center: forceCenter,
  collide: forceCollide,
  nbody: forceManyBody,
  link: forceLink,
  x: forceX,
  y: forceY
};
const Forces = 'forces',
  ForceParams = ['alpha', 'alphaMin', 'alphaTarget', 'velocityDecay', 'forces'],
  ForceConfig = ['static', 'iterations'],
  ForceOutput = ['x', 'y', 'vx', 'vy'];

/**
 * Force simulation layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<object>} params.forces - The forces to apply.
 */
function Force(params) {
  Transform.call(this, null, params);
}
Force.Definition = {
  'type': 'Force',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'static',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'restart',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'iterations',
    'type': 'number',
    'default': 300
  }, {
    'name': 'alpha',
    'type': 'number',
    'default': 1
  }, {
    'name': 'alphaMin',
    'type': 'number',
    'default': 0.001
  }, {
    'name': 'alphaTarget',
    'type': 'number',
    'default': 0
  }, {
    'name': 'velocityDecay',
    'type': 'number',
    'default': 0.4
  }, {
    'name': 'forces',
    'type': 'param',
    'array': true,
    'params': [{
      'key': {
        'force': 'center'
      },
      'params': [{
        'name': 'x',
        'type': 'number',
        'default': 0
      }, {
        'name': 'y',
        'type': 'number',
        'default': 0
      }]
    }, {
      'key': {
        'force': 'collide'
      },
      'params': [{
        'name': 'radius',
        'type': 'number',
        'expr': true
      }, {
        'name': 'strength',
        'type': 'number',
        'default': 0.7
      }, {
        'name': 'iterations',
        'type': 'number',
        'default': 1
      }]
    }, {
      'key': {
        'force': 'nbody'
      },
      'params': [{
        'name': 'strength',
        'type': 'number',
        'default': -30,
        'expr': true
      }, {
        'name': 'theta',
        'type': 'number',
        'default': 0.9
      }, {
        'name': 'distanceMin',
        'type': 'number',
        'default': 1
      }, {
        'name': 'distanceMax',
        'type': 'number'
      }]
    }, {
      'key': {
        'force': 'link'
      },
      'params': [{
        'name': 'links',
        'type': 'data'
      }, {
        'name': 'id',
        'type': 'field'
      }, {
        'name': 'distance',
        'type': 'number',
        'default': 30,
        'expr': true
      }, {
        'name': 'strength',
        'type': 'number',
        'expr': true
      }, {
        'name': 'iterations',
        'type': 'number',
        'default': 1
      }]
    }, {
      'key': {
        'force': 'x'
      },
      'params': [{
        'name': 'strength',
        'type': 'number',
        'default': 0.1
      }, {
        'name': 'x',
        'type': 'field'
      }]
    }, {
      'key': {
        'force': 'y'
      },
      'params': [{
        'name': 'strength',
        'type': 'number',
        'default': 0.1
      }, {
        'name': 'y',
        'type': 'field'
      }]
    }]
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'modify': false,
    'default': ForceOutput
  }]
};
inherits(Force, Transform, {
  transform(_, pulse) {
    var sim = this.value,
      change = pulse.changed(pulse.ADD_REM),
      params = _.modified(ForceParams),
      iters = _.iterations || 300;

    // configure simulation
    if (!sim) {
      this.value = sim = simulation(pulse.source, _);
      sim.on('tick', rerun(pulse.dataflow, this));
      if (!_.static) {
        change = true;
        sim.tick(); // ensure we run on init
      }
      pulse.modifies('index');
    } else {
      if (change) {
        pulse.modifies('index');
        sim.nodes(pulse.source);
      }
      if (params || pulse.changed(pulse.MOD)) {
        setup(sim, _, 0, pulse);
      }
    }

    // run simulation
    if (params || change || _.modified(ForceConfig) || pulse.changed() && _.restart) {
      sim.alpha(Math.max(sim.alpha(), _.alpha || 1)).alphaDecay(1 - Math.pow(sim.alphaMin(), 1 / iters));
      if (_.static) {
        for (sim.stop(); --iters >= 0;) sim.tick();
      } else {
        if (sim.stopped()) sim.restart();
        if (!change) return pulse.StopPropagation; // defer to sim ticks
      }
    }
    return this.finish(_, pulse);
  },
  finish(_, pulse) {
    const dataflow = pulse.dataflow;

    // inspect dependencies, touch link source data
    for (let args = this._argops, j = 0, m = args.length, arg; j < m; ++j) {
      arg = args[j];
      if (arg.name !== Forces || arg.op._argval.force !== 'link') {
        continue;
      }
      for (var ops = arg.op._argops, i = 0, n = ops.length, op; i < n; ++i) {
        if (ops[i].name === 'links' && (op = ops[i].op.source)) {
          dataflow.pulse(op, dataflow.changeset().reflow());
          break;
        }
      }
    }

    // reflow all nodes
    return pulse.reflow(_.modified()).modifies(ForceOutput);
  }
});
function rerun(df, op) {
  return () => df.touch(op).run();
}
function simulation(nodes, _) {
  const sim = forceSimulation(nodes),
    stop = sim.stop,
    restart = sim.restart;
  let stopped = false;
  sim.stopped = () => stopped;
  sim.restart = () => (stopped = false, restart());
  sim.stop = () => (stopped = true, stop());
  return setup(sim, _, true).on('end', () => stopped = true);
}
function setup(sim, _, init, pulse) {
  var f = array$2(_.forces),
    i,
    n,
    p,
    name;
  for (i = 0, n = ForceParams.length; i < n; ++i) {
    p = ForceParams[i];
    if (p !== Forces && _.modified(p)) sim[p](_[p]);
  }
  for (i = 0, n = f.length; i < n; ++i) {
    name = Forces + i;
    p = init || _.modified(Forces, i) ? getForce(f[i]) : pulse && modified(f[i], pulse) ? sim.force(name) : null;
    if (p) sim.force(name, p);
  }
  for (n = sim.numForces || 0; i < n; ++i) {
    sim.force(Forces + i, null); // remove
  }
  sim.numForces = f.length;
  return sim;
}
function modified(f, pulse) {
  var k, v;
  for (k in f) {
    if (isFunction(v = f[k]) && pulse.modified(accessorFields(v))) return 1;
  }
  return 0;
}
function getForce(_) {
  var f, p;
  if (!has$1(ForceMap, _.force)) {
    error$1('Unrecognized force: ' + _.force);
  }
  f = ForceMap[_.force]();
  for (p in _) {
    if (isFunction(f[p])) setForceParam(f[p], _[p], _);
  }
  return f;
}
function setForceParam(f, v, _) {
  f(isFunction(v) ? d => v(d, _) : v);
}

const force = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  force: Force
}, Symbol.toStringTag, { value: 'Module' }));

// Build lookup table mapping tuple keys to tree node instances
function lookup$2 (tree, key, filter) {
  const map = {};
  tree.each(node => {
    const t = node.data;
    if (filter(t)) map[key(t)] = node;
  });
  tree.lookup = map;
  return tree;
}

/**
 * Nest tuples into a tree structure, grouped by key values.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *>} params.keys - The key fields to nest by, in order.
 * @param {boolean} [params.generate=false] - A boolean flag indicating if
 *   non-leaf nodes generated by this transform should be included in the
 *   output. The default (false) includes only the input data (leaf nodes)
 *   in the data stream.
 */
function Nest(params) {
  Transform.call(this, null, params);
}
Nest.Definition = {
  'type': 'Nest',
  'metadata': {
    'treesource': true,
    'changes': true
  },
  'params': [{
    'name': 'keys',
    'type': 'field',
    'array': true
  }, {
    'name': 'generate',
    'type': 'boolean'
  }]
};
const children$1 = n => n.values;
inherits(Nest, Transform, {
  transform(_, pulse) {
    if (!pulse.source) {
      error$1('Nest transform requires an upstream data source.');
    }
    var gen = _.generate,
      mod = _.modified(),
      out = pulse.clone(),
      tree = this.value;
    if (!tree || mod || pulse.changed()) {
      // collect nodes to remove
      if (tree) {
        tree.each(node => {
          if (node.children && isTuple(node.data)) {
            out.rem.push(node.data);
          }
        });
      }

      // generate new tree structure
      this.value = tree = hierarchy({
        values: array$2(_.keys).reduce((n, k) => {
          n.key(k);
          return n;
        }, nest()).entries(out.source)
      }, children$1);

      // collect nodes to add
      if (gen) {
        tree.each(node => {
          if (node.children) {
            node = ingest$1(node.data);
            out.add.push(node);
            out.source.push(node);
          }
        });
      }

      // build lookup table
      lookup$2(tree, tupleid, tupleid);
    }
    out.source.root = tree;
    return out;
  }
});
function nest() {
  const keys = [],
    nest = {
      entries: array => entries(apply(array, 0), 0),
      key: d => (keys.push(d), nest)
    };
  function apply(array, depth) {
    if (depth >= keys.length) {
      return array;
    }
    const n = array.length,
      key = keys[depth++],
      valuesByKey = {},
      result = {};
    let i = -1,
      keyValue,
      value,
      values;
    while (++i < n) {
      keyValue = key(value = array[i]) + '';
      if (values = valuesByKey[keyValue]) {
        values.push(value);
      } else {
        valuesByKey[keyValue] = [value];
      }
    }
    for (keyValue in valuesByKey) {
      result[keyValue] = apply(valuesByKey[keyValue], depth);
    }
    return result;
  }
  function entries(map, depth) {
    if (++depth > keys.length) return map;
    const array = [];
    for (const key in map) {
      array.push({
        key,
        values: entries(map[key], depth)
      });
    }
    return array;
  }
  return nest;
}

/**
 * Abstract class for tree layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function HierarchyLayout(params) {
  Transform.call(this, null, params);
}
const defaultSeparation = (a, b) => a.parent === b.parent ? 1 : 2;
inherits(HierarchyLayout, Transform, {
  transform(_, pulse) {
    if (!pulse.source || !pulse.source.root) {
      error$1(this.constructor.name + ' transform requires a backing tree data source.');
    }
    const layout = this.layout(_.method),
      fields = this.fields,
      root = pulse.source.root,
      as = _.as || fields;
    if (_.field) root.sum(_.field);else root.count();
    if (_.sort) root.sort(stableCompare(_.sort, d => d.data));
    setParams(layout, this.params, _);
    if (layout.separation) {
      layout.separation(_.separation !== false ? defaultSeparation : one$1);
    }
    try {
      this.value = layout(root);
    } catch (err) {
      error$1(err);
    }
    root.each(node => setFields(node, fields, as));
    return pulse.reflow(_.modified()).modifies(as).modifies('leaf');
  }
});
function setParams(layout, params, _) {
  for (let p, i = 0, n = params.length; i < n; ++i) {
    p = params[i];
    if (p in _) layout[p](_[p]);
  }
}
function setFields(node, fields, as) {
  const t = node.data,
    n = fields.length - 1;
  for (let i = 0; i < n; ++i) {
    t[as[i]] = node[fields[i]];
  }
  t[as[n]] = node.children ? node.children.length : 0;
}

const Output$3 = ['x', 'y', 'r', 'depth', 'children'];

/**
 * Packed circle tree layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to size nodes.
 */
function Pack(params) {
  HierarchyLayout.call(this, params);
}
Pack.Definition = {
  'type': 'Pack',
  'metadata': {
    'tree': true,
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'sort',
    'type': 'compare'
  }, {
    'name': 'padding',
    'type': 'number',
    'default': 0
  }, {
    'name': 'radius',
    'type': 'field',
    'default': null
  }, {
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': Output$3.length,
    'default': Output$3
  }]
};
inherits(Pack, HierarchyLayout, {
  layout: pack,
  params: ['radius', 'size', 'padding'],
  fields: Output$3
});

const Output$2 = ['x0', 'y0', 'x1', 'y1', 'depth', 'children'];

/**
 * Partition tree layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to size nodes.
 */
function Partition(params) {
  HierarchyLayout.call(this, params);
}
Partition.Definition = {
  'type': 'Partition',
  'metadata': {
    'tree': true,
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'sort',
    'type': 'compare'
  }, {
    'name': 'padding',
    'type': 'number',
    'default': 0
  }, {
    'name': 'round',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': Output$2.length,
    'default': Output$2
  }]
};
inherits(Partition, HierarchyLayout, {
  layout: partition$4,
  params: ['size', 'round', 'padding'],
  fields: Output$2
});

/**
 * Stratify a collection of tuples into a tree structure based on
 * id and parent id fields.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.key - Unique key field for each tuple.
 * @param {function(object): *} params.parentKey - Field with key for parent tuple.
 */
function Stratify(params) {
  Transform.call(this, null, params);
}
Stratify.Definition = {
  'type': 'Stratify',
  'metadata': {
    'treesource': true
  },
  'params': [{
    'name': 'key',
    'type': 'field',
    'required': true
  }, {
    'name': 'parentKey',
    'type': 'field',
    'required': true
  }]
};
inherits(Stratify, Transform, {
  transform(_, pulse) {
    if (!pulse.source) {
      error$1('Stratify transform requires an upstream data source.');
    }
    let tree = this.value;
    const mod = _.modified(),
      out = pulse.fork(pulse.ALL).materialize(pulse.SOURCE),
      run = !tree || mod || pulse.changed(pulse.ADD_REM) || pulse.modified(_.key.fields) || pulse.modified(_.parentKey.fields);

    // prevent upstream source pollution
    out.source = out.source.slice();
    if (run) {
      tree = out.source.length ? lookup$2(stratify().id(_.key).parentId(_.parentKey)(out.source), _.key, truthy) : lookup$2(stratify()([{}]), _.key, _.key);
    }
    out.source.root = this.value = tree;
    return out;
  }
});

const Layouts = {
  tidy: tree$1,
  cluster: cluster
};
const Output$1$1 = ['x', 'y', 'depth', 'children'];

/**
 * Tree layout. Depending on the method parameter, performs either
 * Reingold-Tilford 'tidy' layout or dendrogram 'cluster' layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function Tree(params) {
  HierarchyLayout.call(this, params);
}
Tree.Definition = {
  'type': 'Tree',
  'metadata': {
    'tree': true,
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'sort',
    'type': 'compare'
  }, {
    'name': 'method',
    'type': 'enum',
    'default': 'tidy',
    'values': ['tidy', 'cluster']
  }, {
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'nodeSize',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'separation',
    'type': 'boolean',
    'default': true
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': Output$1$1.length,
    'default': Output$1$1
  }]
};
inherits(Tree, HierarchyLayout, {
  /**
   * Tree layout generator. Supports both 'tidy' and 'cluster' layouts.
   */
  layout(method) {
    const m = method || 'tidy';
    if (has$1(Layouts, m)) return Layouts[m]();else error$1('Unrecognized Tree layout method: ' + m);
  },
  params: ['size', 'nodeSize'],
  fields: Output$1$1
});

/**
 * Generate tuples representing links between tree nodes.
 * The resulting tuples will contain 'source' and 'target' fields,
 * which point to parent and child node tuples, respectively.
 * @constructor
 * @param {object} params - The parameters for this operator.
 */
function TreeLinks(params) {
  Transform.call(this, [], params);
}
TreeLinks.Definition = {
  'type': 'TreeLinks',
  'metadata': {
    'tree': true,
    'generates': true,
    'changes': true
  },
  'params': []
};
inherits(TreeLinks, Transform, {
  transform(_, pulse) {
    const links = this.value,
      tree = pulse.source && pulse.source.root,
      out = pulse.fork(pulse.NO_SOURCE),
      lut = {};
    if (!tree) error$1('TreeLinks transform requires a tree data source.');
    if (pulse.changed(pulse.ADD_REM)) {
      // remove previous links
      out.rem = links;

      // build lookup table of valid tuples
      pulse.visit(pulse.SOURCE, t => lut[tupleid(t)] = 1);

      // generate links for all edges incident on valid tuples
      tree.each(node => {
        const t = node.data,
          p = node.parent && node.parent.data;
        if (p && lut[tupleid(t)] && lut[tupleid(p)]) {
          out.add.push(ingest$1({
            source: p,
            target: t
          }));
        }
      });
      this.value = out.add;
    } else if (pulse.changed(pulse.MOD)) {
      // build lookup table of modified tuples
      pulse.visit(pulse.MOD, t => lut[tupleid(t)] = 1);

      // gather links incident on modified tuples
      links.forEach(link => {
        if (lut[tupleid(link.source)] || lut[tupleid(link.target)]) {
          out.mod.push(link);
        }
      });
    }
    return out;
  }
});

const Tiles = {
  binary: treemapBinary,
  dice: treemapDice,
  slice: treemapSlice,
  slicedice: treemapSliceDice,
  squarify: treemapSquarify,
  resquarify: treemapResquarify
};
const Output$4 = ['x0', 'y0', 'x1', 'y1', 'depth', 'children'];

/**
 * Treemap layout.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.field - The value field to size nodes.
 */
function Treemap(params) {
  HierarchyLayout.call(this, params);
}
Treemap.Definition = {
  'type': 'Treemap',
  'metadata': {
    'tree': true,
    'modifies': true
  },
  'params': [{
    'name': 'field',
    'type': 'field'
  }, {
    'name': 'sort',
    'type': 'compare'
  }, {
    'name': 'method',
    'type': 'enum',
    'default': 'squarify',
    'values': ['squarify', 'resquarify', 'binary', 'dice', 'slice', 'slicedice']
  }, {
    'name': 'padding',
    'type': 'number',
    'default': 0
  }, {
    'name': 'paddingInner',
    'type': 'number',
    'default': 0
  }, {
    'name': 'paddingOuter',
    'type': 'number',
    'default': 0
  }, {
    'name': 'paddingTop',
    'type': 'number',
    'default': 0
  }, {
    'name': 'paddingRight',
    'type': 'number',
    'default': 0
  }, {
    'name': 'paddingBottom',
    'type': 'number',
    'default': 0
  }, {
    'name': 'paddingLeft',
    'type': 'number',
    'default': 0
  }, {
    'name': 'ratio',
    'type': 'number',
    'default': 1.618033988749895
  }, {
    'name': 'round',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': Output$4.length,
    'default': Output$4
  }]
};
inherits(Treemap, HierarchyLayout, {
  /**
   * Treemap layout generator. Adds 'method' and 'ratio' parameters
   * to configure the underlying tile method.
   */
  layout() {
    const x = treemap();
    x.ratio = _ => {
      const t = x.tile();
      if (t.ratio) x.tile(t.ratio(_));
    };
    x.method = _ => {
      if (has$1(Tiles, _)) x.tile(Tiles[_]);else error$1('Unrecognized Treemap layout method: ' + _);
    };
    return x;
  },
  params: ['method', 'ratio', 'size', 'round', 'padding', 'paddingInner', 'paddingOuter', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
  fields: Output$4
});

const tree = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  nest: Nest,
  pack: Pack,
  partition: Partition,
  stratify: Stratify,
  tree: Tree,
  treelinks: TreeLinks,
  treemap: Treemap
}, Symbol.toStringTag, { value: 'Module' }));

// bit mask for getting first 2 bytes of alpha value
const ALPHA_MASK = 0xff000000;
function baseBitmaps($, data) {
  const bitmap = $.bitmap();
  // when there is no base mark but data points are to be avoided
  (data || []).forEach(d => bitmap.set($(d.boundary[0]), $(d.boundary[3])));
  return [bitmap, undefined];
}
function markBitmaps($, baseMark, avoidMarks, labelInside, isGroupArea) {
  // create canvas
  const width = $.width,
    height = $.height,
    border = labelInside || isGroupArea,
    context = domCanvas(width, height).getContext('2d'),
    baseMarkContext = domCanvas(width, height).getContext('2d'),
    strokeContext = border && domCanvas(width, height).getContext('2d');

  // render all marks to be avoided into canvas
  avoidMarks.forEach(items => draw(context, items, false));
  draw(baseMarkContext, baseMark, false);
  if (border) {
    draw(strokeContext, baseMark, true);
  }

  // get canvas buffer, create bitmaps
  const buffer = getBuffer(context, width, height),
    baseMarkBuffer = getBuffer(baseMarkContext, width, height),
    strokeBuffer = border && getBuffer(strokeContext, width, height),
    layer1 = $.bitmap(),
    layer2 = border && $.bitmap();

  // populate bitmap layers
  let x, y, u, v, index, alpha, strokeAlpha, baseMarkAlpha;
  for (y = 0; y < height; ++y) {
    for (x = 0; x < width; ++x) {
      index = y * width + x;
      alpha = buffer[index] & ALPHA_MASK;
      baseMarkAlpha = baseMarkBuffer[index] & ALPHA_MASK;
      strokeAlpha = border && strokeBuffer[index] & ALPHA_MASK;
      if (alpha || strokeAlpha || baseMarkAlpha) {
        u = $(x);
        v = $(y);
        if (!isGroupArea && (alpha || baseMarkAlpha)) layer1.set(u, v); // update interior bitmap
        if (border && (alpha || strokeAlpha)) layer2.set(u, v); // update border bitmap
      }
    }
  }
  return [layer1, layer2];
}
function getBuffer(context, width, height) {
  return new Uint32Array(context.getImageData(0, 0, width, height).data.buffer);
}
function draw(context, items, interior) {
  if (!items.length) return;
  const type = items[0].mark.marktype;
  if (type === 'group') {
    items.forEach(group => {
      group.items.forEach(mark => draw(context, mark.items, interior));
    });
  } else {
    Marks[type].draw(context, {
      items: interior ? items.map(prepare) : items
    });
  }
}

/**
 * Prepare item before drawing into canvas (setting stroke and opacity)
 * @param {object} source item to be prepared
 * @returns prepared item
 */
function prepare(source) {
  const item = rederive(source, {});
  if (item.stroke && item.strokeOpacity !== 0 || item.fill && item.fillOpacity !== 0) {
    return {
      ...item,
      strokeOpacity: 1,
      stroke: '#000',
      fillOpacity: 0
    };
  }
  return item;
}

const DIV = 5,
  // bit shift from x, y index to bit vector array index
  MOD = 31,
  // bit mask for index lookup within a bit vector
  SIZE$1 = 32,
  // individual bit vector size
  RIGHT0 = new Uint32Array(SIZE$1 + 1),
  // left-anchored bit vectors, full -> 0
  RIGHT1 = new Uint32Array(SIZE$1 + 1); // right-anchored bit vectors, 0 -> full

RIGHT1[0] = 0;
RIGHT0[0] = ~RIGHT1[0];
for (let i = 1; i <= SIZE$1; ++i) {
  RIGHT1[i] = RIGHT1[i - 1] << 1 | 1;
  RIGHT0[i] = ~RIGHT1[i];
}
function Bitmap (w, h) {
  const array = new Uint32Array(~~((w * h + SIZE$1) / SIZE$1));
  function _set(index, mask) {
    array[index] |= mask;
  }
  function _clear(index, mask) {
    array[index] &= mask;
  }
  return {
    array: array,
    get: (x, y) => {
      const index = y * w + x;
      return array[index >>> DIV] & 1 << (index & MOD);
    },
    set: (x, y) => {
      const index = y * w + x;
      _set(index >>> DIV, 1 << (index & MOD));
    },
    clear: (x, y) => {
      const index = y * w + x;
      _clear(index >>> DIV, ~(1 << (index & MOD)));
    },
    getRange: (x, y, x2, y2) => {
      let r = y2,
        start,
        end,
        indexStart,
        indexEnd;
      for (; r >= y; --r) {
        start = r * w + x;
        end = r * w + x2;
        indexStart = start >>> DIV;
        indexEnd = end >>> DIV;
        if (indexStart === indexEnd) {
          if (array[indexStart] & RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]) {
            return true;
          }
        } else {
          if (array[indexStart] & RIGHT0[start & MOD]) return true;
          if (array[indexEnd] & RIGHT1[(end & MOD) + 1]) return true;
          for (let i = indexStart + 1; i < indexEnd; ++i) {
            if (array[i]) return true;
          }
        }
      }
      return false;
    },
    setRange: (x, y, x2, y2) => {
      let start, end, indexStart, indexEnd, i;
      for (; y <= y2; ++y) {
        start = y * w + x;
        end = y * w + x2;
        indexStart = start >>> DIV;
        indexEnd = end >>> DIV;
        if (indexStart === indexEnd) {
          _set(indexStart, RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]);
        } else {
          _set(indexStart, RIGHT0[start & MOD]);
          _set(indexEnd, RIGHT1[(end & MOD) + 1]);
          for (i = indexStart + 1; i < indexEnd; ++i) _set(i, 0xffffffff);
        }
      }
    },
    clearRange: (x, y, x2, y2) => {
      let start, end, indexStart, indexEnd, i;
      for (; y <= y2; ++y) {
        start = y * w + x;
        end = y * w + x2;
        indexStart = start >>> DIV;
        indexEnd = end >>> DIV;
        if (indexStart === indexEnd) {
          _clear(indexStart, RIGHT1[start & MOD] | RIGHT0[(end & MOD) + 1]);
        } else {
          _clear(indexStart, RIGHT1[start & MOD]);
          _clear(indexEnd, RIGHT0[(end & MOD) + 1]);
          for (i = indexStart + 1; i < indexEnd; ++i) _clear(i, 0);
        }
      }
    },
    outOfBounds: (x, y, x2, y2) => x < 0 || y < 0 || y2 >= h || x2 >= w
  };
}

function scaler (width, height, padding) {
  const ratio = Math.max(1, Math.sqrt(width * height / 1e6)),
    w = ~~((width + 2 * padding + ratio) / ratio),
    h = ~~((height + 2 * padding + ratio) / ratio),
    scale = _ => ~~((_ + padding) / ratio);
  scale.invert = _ => _ * ratio - padding;
  scale.bitmap = () => Bitmap(w, h);
  scale.ratio = ratio;
  scale.padding = padding;
  scale.width = width;
  scale.height = height;
  return scale;
}

function placeAreaLabelNaive ($, bitmaps, avoidBaseMark, markIndex) {
  const width = $.width,
    height = $.height;

  // try to place a label within an input area mark
  return function (d) {
    const items = d.datum.datum.items[markIndex].items,
      // area points
      n = items.length,
      // number of points
      textHeight = d.datum.fontSize,
      // label width
      textWidth = textMetrics.width(d.datum, d.datum.text); // label height

    let maxAreaWidth = 0,
      x1,
      x2,
      y1,
      y2,
      x,
      y,
      areaWidth;

    // for each area sample point
    for (let i = 0; i < n; ++i) {
      x1 = items[i].x;
      y1 = items[i].y;
      x2 = items[i].x2 === undefined ? x1 : items[i].x2;
      y2 = items[i].y2 === undefined ? y1 : items[i].y2;
      x = (x1 + x2) / 2;
      y = (y1 + y2) / 2;
      areaWidth = Math.abs(x2 - x1 + y2 - y1);
      if (areaWidth >= maxAreaWidth) {
        maxAreaWidth = areaWidth;
        d.x = x;
        d.y = y;
      }
    }
    x = textWidth / 2;
    y = textHeight / 2;
    x1 = d.x - x;
    x2 = d.x + x;
    y1 = d.y - y;
    y2 = d.y + y;
    d.align = 'center';
    if (x1 < 0 && x2 <= width) {
      d.align = 'left';
    } else if (0 <= x1 && width < x2) {
      d.align = 'right';
    }
    d.baseline = 'middle';
    if (y1 < 0 && y2 <= height) {
      d.baseline = 'top';
    } else if (0 <= y1 && height < y2) {
      d.baseline = 'bottom';
    }
    return true;
  };
}

function outOfBounds(x, y, textWidth, textHeight, width, height) {
  let r = textWidth / 2;
  return x - r < 0 || x + r > width || y - (r = textHeight / 2) < 0 || y + r > height;
}
function collision($, x, y, textHeight, textWidth, h, bm0, bm1) {
  const w = textWidth * h / (textHeight * 2),
    x1 = $(x - w),
    x2 = $(x + w),
    y1 = $(y - (h = h / 2)),
    y2 = $(y + h);
  return bm0.outOfBounds(x1, y1, x2, y2) || bm0.getRange(x1, y1, x2, y2) || bm1 && bm1.getRange(x1, y1, x2, y2);
}

function placeAreaLabelReducedSearch ($, bitmaps, avoidBaseMark, markIndex) {
  const width = $.width,
    height = $.height,
    bm0 = bitmaps[0],
    // where labels have been placed
    bm1 = bitmaps[1]; // area outlines

  function tryLabel(_x, _y, maxSize, textWidth, textHeight) {
    const x = $.invert(_x),
      y = $.invert(_y);
    let lo = maxSize,
      hi = height,
      mid;
    if (!outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, lo, bm0, bm1) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) {
      // if the label fits at the current sample point,
      // perform binary search to find the largest font size that fits
      while (hi - lo >= 1) {
        mid = (lo + hi) / 2;
        if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) {
          hi = mid;
        } else {
          lo = mid;
        }
      }
      // place label if current lower bound exceeds prior max font size
      if (lo > maxSize) {
        return [x, y, lo, true];
      }
    }
  }

  // try to place a label within an input area mark
  return function (d) {
    const items = d.datum.datum.items[markIndex].items,
      // area points
      n = items.length,
      // number of points
      textHeight = d.datum.fontSize,
      // label width
      textWidth = textMetrics.width(d.datum, d.datum.text); // label height

    let maxSize = avoidBaseMark ? textHeight : 0,
      labelPlaced = false,
      labelPlaced2 = false,
      maxAreaWidth = 0,
      x1,
      x2,
      y1,
      y2,
      x,
      y,
      _x,
      _y,
      _x1,
      _xMid,
      _x2,
      _y1,
      _yMid,
      _y2,
      areaWidth,
      result,
      swapTmp;

    // for each area sample point
    for (let i = 0; i < n; ++i) {
      x1 = items[i].x;
      y1 = items[i].y;
      x2 = items[i].x2 === undefined ? x1 : items[i].x2;
      y2 = items[i].y2 === undefined ? y1 : items[i].y2;
      if (x1 > x2) {
        swapTmp = x1;
        x1 = x2;
        x2 = swapTmp;
      }
      if (y1 > y2) {
        swapTmp = y1;
        y1 = y2;
        y2 = swapTmp;
      }
      _x1 = $(x1);
      _x2 = $(x2);
      _xMid = ~~((_x1 + _x2) / 2);
      _y1 = $(y1);
      _y2 = $(y2);
      _yMid = ~~((_y1 + _y2) / 2);

      // search along the line from mid point between the 2 border to lower border
      for (_x = _xMid; _x >= _x1; --_x) {
        for (_y = _yMid; _y >= _y1; --_y) {
          result = tryLabel(_x, _y, maxSize, textWidth, textHeight);
          if (result) {
            [d.x, d.y, maxSize, labelPlaced] = result;
          }
        }
      }

      // search along the line from mid point between the 2 border to upper border
      for (_x = _xMid; _x <= _x2; ++_x) {
        for (_y = _yMid; _y <= _y2; ++_y) {
          result = tryLabel(_x, _y, maxSize, textWidth, textHeight);
          if (result) {
            [d.x, d.y, maxSize, labelPlaced] = result;
          }
        }
      }

      // place label at slice center if not placed through other means
      // and if we're not avoiding overlap with other areas
      if (!labelPlaced && !avoidBaseMark) {
        // one span is zero, hence we can add
        areaWidth = Math.abs(x2 - x1 + y2 - y1);
        x = (x1 + x2) / 2;
        y = (y1 + y2) / 2;

        // place label if it fits and improves the max area width
        if (areaWidth >= maxAreaWidth && !outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) {
          maxAreaWidth = areaWidth;
          d.x = x;
          d.y = y;
          labelPlaced2 = true;
        }
      }
    }

    // record current label placement information, update label bitmap
    if (labelPlaced || labelPlaced2) {
      x = textWidth / 2;
      y = textHeight / 2;
      bm0.setRange($(d.x - x), $(d.y - y), $(d.x + x), $(d.y + y));
      d.align = 'center';
      d.baseline = 'middle';
      return true;
    } else {
      return false;
    }
  };
}

// pixel direction offsets for flood fill search
const X_DIR = [-1, -1, 1, 1];
const Y_DIR = [-1, 1, -1, 1];
function placeAreaLabelFloodFill ($, bitmaps, avoidBaseMark, markIndex) {
  const width = $.width,
    height = $.height,
    bm0 = bitmaps[0],
    // where labels have been placed
    bm1 = bitmaps[1],
    // area outlines
    bm2 = $.bitmap(); // flood-fill visitations

  // try to place a label within an input area mark
  return function (d) {
    const items = d.datum.datum.items[markIndex].items,
      // area points
      n = items.length,
      // number of points
      textHeight = d.datum.fontSize,
      // label width
      textWidth = textMetrics.width(d.datum, d.datum.text),
      // label height
      stack = []; // flood fill stack

    let maxSize = avoidBaseMark ? textHeight : 0,
      labelPlaced = false,
      labelPlaced2 = false,
      maxAreaWidth = 0,
      x1,
      x2,
      y1,
      y2,
      x,
      y,
      _x,
      _y,
      lo,
      hi,
      mid,
      areaWidth;

    // for each area sample point
    for (let i = 0; i < n; ++i) {
      x1 = items[i].x;
      y1 = items[i].y;
      x2 = items[i].x2 === undefined ? x1 : items[i].x2;
      y2 = items[i].y2 === undefined ? y1 : items[i].y2;

      // add scaled center point to stack
      stack.push([$((x1 + x2) / 2), $((y1 + y2) / 2)]);

      // perform flood fill, visit points
      while (stack.length) {
        [_x, _y] = stack.pop();

        // exit if point already marked
        if (bm0.get(_x, _y) || bm1.get(_x, _y) || bm2.get(_x, _y)) continue;

        // mark point in flood fill bitmap
        // add search points for all (in bound) directions
        bm2.set(_x, _y);
        for (let j = 0; j < 4; ++j) {
          x = _x + X_DIR[j];
          y = _y + Y_DIR[j];
          if (!bm2.outOfBounds(x, y, x, y)) stack.push([x, y]);
        }

        // unscale point back to x, y space
        x = $.invert(_x);
        y = $.invert(_y);
        lo = maxSize;
        hi = height; // TODO: make this bound smaller

        if (!outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, lo, bm0, bm1) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) {
          // if the label fits at the current sample point,
          // perform binary search to find the largest font size that fits
          while (hi - lo >= 1) {
            mid = (lo + hi) / 2;
            if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) {
              hi = mid;
            } else {
              lo = mid;
            }
          }
          // place label if current lower bound exceeds prior max font size
          if (lo > maxSize) {
            d.x = x;
            d.y = y;
            maxSize = lo;
            labelPlaced = true;
          }
        }
      }

      // place label at slice center if not placed through other means
      // and if we're not avoiding overlap with other areas
      if (!labelPlaced && !avoidBaseMark) {
        // one span is zero, hence we can add
        areaWidth = Math.abs(x2 - x1 + y2 - y1);
        x = (x1 + x2) / 2;
        y = (y1 + y2) / 2;

        // place label if it fits and improves the max area width
        if (areaWidth >= maxAreaWidth && !outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) {
          maxAreaWidth = areaWidth;
          d.x = x;
          d.y = y;
          labelPlaced2 = true;
        }
      }
    }

    // record current label placement information, update label bitmap
    if (labelPlaced || labelPlaced2) {
      x = textWidth / 2;
      y = textHeight / 2;
      bm0.setRange($(d.x - x), $(d.y - y), $(d.x + x), $(d.y + y));
      d.align = 'center';
      d.baseline = 'middle';
      return true;
    } else {
      return false;
    }
  };
}

const Aligns = ['right', 'center', 'left'],
  Baselines = ['bottom', 'middle', 'top'];
function placeMarkLabel ($, bitmaps, anchors, offsets) {
  const width = $.width,
    height = $.height,
    bm0 = bitmaps[0],
    bm1 = bitmaps[1],
    n = offsets.length;
  return function (d) {
    const boundary = d.boundary,
      textHeight = d.datum.fontSize;

    // can not be placed if the mark is not visible in the graph bound
    if (boundary[2] < 0 || boundary[5] < 0 || boundary[0] > width || boundary[3] > height) {
      return false;
    }
    let textWidth = d.textWidth ?? 0,
      dx,
      dy,
      isInside,
      sizeFactor,
      insideFactor,
      x1,
      x2,
      y1,
      y2,
      xc,
      yc,
      _x1,
      _x2,
      _y1,
      _y2;

    // for each anchor and offset
    for (let i = 0; i < n; ++i) {
      dx = (anchors[i] & 0x3) - 1;
      dy = (anchors[i] >>> 0x2 & 0x3) - 1;
      isInside = dx === 0 && dy === 0 || offsets[i] < 0;
      sizeFactor = dx && dy ? Math.SQRT1_2 : 1;
      insideFactor = offsets[i] < 0 ? -1 : 1;
      x1 = boundary[1 + dx] + offsets[i] * dx * sizeFactor;
      yc = boundary[4 + dy] + insideFactor * textHeight * dy / 2 + offsets[i] * dy * sizeFactor;
      y1 = yc - textHeight / 2;
      y2 = yc + textHeight / 2;
      _x1 = $(x1);
      _y1 = $(y1);
      _y2 = $(y2);
      if (!textWidth) {
        // to avoid finding width of text label,
        if (!test(_x1, _x1, _y1, _y2, bm0, bm1, x1, x1, y1, y2, boundary, isInside)) {
          // skip this anchor/offset option if we fail to place a label with 1px width
          continue;
        } else {
          // Otherwise, find the label width
          textWidth = textMetrics.width(d.datum, d.datum.text);
        }
      }
      xc = x1 + insideFactor * textWidth * dx / 2;
      x1 = xc - textWidth / 2;
      x2 = xc + textWidth / 2;
      _x1 = $(x1);
      _x2 = $(x2);
      if (test(_x1, _x2, _y1, _y2, bm0, bm1, x1, x2, y1, y2, boundary, isInside)) {
        // place label if the position is placeable
        d.x = !dx ? xc : dx * insideFactor < 0 ? x2 : x1;
        d.y = !dy ? yc : dy * insideFactor < 0 ? y2 : y1;
        d.align = Aligns[dx * insideFactor + 1];
        d.baseline = Baselines[dy * insideFactor + 1];
        bm0.setRange(_x1, _y1, _x2, _y2);
        return true;
      }
    }
    return false;
  };
}

// Test if a label with the given dimensions can be added without overlap
function test(_x1, _x2, _y1, _y2, bm0, bm1, x1, x2, y1, y2, boundary, isInside) {
  return !(bm0.outOfBounds(_x1, _y1, _x2, _y2) || (isInside && bm1 || bm0).getRange(_x1, _y1, _x2, _y2));
}

// 8-bit representation of anchors
const TOP = 0x0,
  MIDDLE = 0x4,
  BOTTOM = 0x8,
  LEFT = 0x0,
  CENTER$1 = 0x1,
  RIGHT = 0x2;

// Mapping from text anchor to number representation
const anchorCode = {
  'top-left': TOP + LEFT,
  'top': TOP + CENTER$1,
  'top-right': TOP + RIGHT,
  'left': MIDDLE + LEFT,
  'middle': MIDDLE + CENTER$1,
  'right': MIDDLE + RIGHT,
  'bottom-left': BOTTOM + LEFT,
  'bottom': BOTTOM + CENTER$1,
  'bottom-right': BOTTOM + RIGHT
};
const placeAreaLabel = {
  'naive': placeAreaLabelNaive,
  'reduced-search': placeAreaLabelReducedSearch,
  'floodfill': placeAreaLabelFloodFill
};
function labelLayout (texts, size, compare, offset, anchor, avoidMarks, avoidBaseMark, lineAnchor, markIndex, padding, method) {
  // early exit for empty data
  if (!texts.length) return texts;
  const positions = Math.max(offset.length, anchor.length),
    offsets = getOffsets(offset, positions),
    anchors = getAnchors(anchor, positions),
    marktype = markType(texts[0].datum),
    grouptype = marktype === 'group' && texts[0].datum.items[markIndex].marktype,
    isGroupArea = grouptype === 'area',
    boundary = markBoundary(marktype, grouptype, lineAnchor, markIndex),
    infPadding = padding === null || padding === Infinity,
    isNaiveGroupArea = isGroupArea && method === 'naive';
  let maxTextWidth = -1,
    maxTextHeight = -1;

  // prepare text mark data for placing
  const data = texts.map(d => {
    const textWidth = infPadding ? textMetrics.width(d, d.text) : undefined;
    maxTextWidth = Math.max(maxTextWidth, textWidth);
    maxTextHeight = Math.max(maxTextHeight, d.fontSize);
    return {
      datum: d,
      opacity: 0,
      x: undefined,
      y: undefined,
      align: undefined,
      baseline: undefined,
      boundary: boundary(d),
      textWidth
    };
  });
  padding = padding === null || padding === Infinity ? Math.max(maxTextWidth, maxTextHeight) + Math.max(...offset) : padding;
  const $ = scaler(size[0], size[1], padding);
  let bitmaps;
  if (!isNaiveGroupArea) {
    // sort labels in priority order, if comparator is provided
    if (compare) {
      data.sort((a, b) => compare(a.datum, b.datum));
    }

    // flag indicating if label can be placed inside its base mark
    let labelInside = false;
    for (let i = 0; i < anchors.length && !labelInside; ++i) {
      // label inside if anchor is at center
      // label inside if offset to be inside the mark bound
      labelInside = anchors[i] === 0x5 || offsets[i] < 0;
    }

    // extract data information from base mark when base mark is to be avoided
    // base mark is implicitly avoided if it is a group area
    const baseMark = (marktype && avoidBaseMark || isGroupArea) && texts.map(d => d.datum);

    // generate bitmaps for layout calculation
    bitmaps = avoidMarks.length || baseMark ? markBitmaps($, baseMark || [], avoidMarks, labelInside, isGroupArea) : baseBitmaps($, avoidBaseMark && data);
  }

  // generate label placement function
  const place = isGroupArea ? placeAreaLabel[method]($, bitmaps, avoidBaseMark, markIndex) : placeMarkLabel($, bitmaps, anchors, offsets);

  // place all labels
  data.forEach(d => d.opacity = +place(d));
  return data;
}
function getOffsets(_, count) {
  const offsets = new Float64Array(count),
    n = _.length;
  for (let i = 0; i < n; ++i) offsets[i] = _[i] || 0;
  for (let i = n; i < count; ++i) offsets[i] = offsets[n - 1];
  return offsets;
}
function getAnchors(_, count) {
  const anchors = new Int8Array(count),
    n = _.length;
  for (let i = 0; i < n; ++i) anchors[i] |= anchorCode[_[i]];
  for (let i = n; i < count; ++i) anchors[i] = anchors[n - 1];
  return anchors;
}
function markType(item) {
  return item && item.mark && item.mark.marktype;
}

/**
 * Factory function for function for getting base mark boundary, depending
 * on mark and group type. When mark type is undefined, line or area: boundary
 * is the coordinate of each data point. When base mark is grouped line,
 * boundary is either at the start or end of the line depending on the
 * value of lineAnchor. Otherwise, use bounds of base mark.
 */
function markBoundary(marktype, grouptype, lineAnchor, markIndex) {
  const xy = d => [d.x, d.x, d.x, d.y, d.y, d.y];
  if (!marktype) {
    return xy; // no reactive geometry
  } else if (marktype === 'line' || marktype === 'area') {
    return d => xy(d.datum);
  } else if (grouptype === 'line') {
    return d => {
      const items = d.datum.items[markIndex].items;
      return xy(items.length ? items[lineAnchor === 'start' ? 0 : items.length - 1] : {
        x: NaN,
        y: NaN
      });
    };
  } else {
    return d => {
      const b = d.datum.bounds;
      return [b.x1, (b.x1 + b.x2) / 2, b.x2, b.y1, (b.y1 + b.y2) / 2, b.y2];
    };
  }
}

const Output$1 = ['x', 'y', 'opacity', 'align', 'baseline'];
const Anchors = ['top-left', 'left', 'bottom-left', 'top', 'bottom', 'top-right', 'right', 'bottom-right'];

/**
 * Compute text label layout to annotate marks.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<number>} params.size - The size of the layout, provided as a [width, height] array.
 * @param {function(*,*): number} [params.sort] - An optional
 *   comparator function for sorting label data in priority order.
 * @param {Array<string>} [params.anchor] - Label anchor points relative to the base mark bounding box.
 *   The available options are 'top-left', 'left', 'bottom-left', 'top',
 *   'bottom', 'top-right', 'right', 'bottom-right', 'middle'.
 * @param {Array<number>} [params.offset] - Label offsets (in pixels) from the base mark bounding box.
 *   This parameter is parallel to the list of anchor points.
 * @param {number | null} [params.padding=0] - The amount (in pixels) that a label may exceed the layout size.
 *   If this parameter is null, a label may exceed the layout size without any boundary.
 * @param {string} [params.lineAnchor='end'] - For group line mark labels only, indicates the anchor
 *   position for labels. One of 'start' or 'end'.
 * @param {string} [params.markIndex=0] - For group mark labels only, an index indicating
 *   which mark within the group should be labeled.
 * @param {Array<number>} [params.avoidMarks] - A list of additional mark names for which the label
 *   layout should avoid overlap.
 * @param {boolean} [params.avoidBaseMark=true] - Boolean flag indicating if labels should avoid
 *   overlap with the underlying base mark being labeled.
 * @param {string} [params.method='naive'] - For area make labels only, a method for
 *   place labels. One of 'naive', 'reduced-search', or 'floodfill'.
 * @param {Array<string>} [params.as] - The output fields written by the transform.
 *   The default is ['x', 'y', 'opacity', 'align', 'baseline'].
 */
function Label$1(params) {
  Transform.call(this, null, params);
}
Label$1.Definition = {
  type: 'Label',
  metadata: {
    modifies: true
  },
  params: [{
    name: 'size',
    type: 'number',
    array: true,
    length: 2,
    required: true
  }, {
    name: 'sort',
    type: 'compare'
  }, {
    name: 'anchor',
    type: 'string',
    array: true,
    default: Anchors
  }, {
    name: 'offset',
    type: 'number',
    array: true,
    default: [1]
  }, {
    name: 'padding',
    type: 'number',
    default: 0,
    null: true
  }, {
    name: 'lineAnchor',
    type: 'string',
    values: ['start', 'end'],
    default: 'end'
  }, {
    name: 'markIndex',
    type: 'number',
    default: 0
  }, {
    name: 'avoidBaseMark',
    type: 'boolean',
    default: true
  }, {
    name: 'avoidMarks',
    type: 'data',
    array: true
  }, {
    name: 'method',
    type: 'string',
    default: 'naive'
  }, {
    name: 'as',
    type: 'string',
    array: true,
    length: Output$1.length,
    default: Output$1
  }]
};
inherits(Label$1, Transform, {
  transform(_, pulse) {
    function modp(param) {
      const p = _[param];
      return isFunction(p) && pulse.modified(p.fields);
    }
    const mod = _.modified();
    if (!(mod || pulse.changed(pulse.ADD_REM) || modp('sort'))) return;
    if (!_.size || _.size.length !== 2) {
      error$1('Size parameter should be specified as a [width, height] array.');
    }
    const as = _.as || Output$1;

    // run label layout
    labelLayout(pulse.materialize(pulse.SOURCE).source || [], _.size, _.sort, array$2(_.offset == null ? 1 : _.offset), array$2(_.anchor || Anchors), _.avoidMarks || [], _.avoidBaseMark !== false, _.lineAnchor || 'end', _.markIndex || 0, _.padding === undefined ? 0 : _.padding, _.method || 'naive').forEach(l => {
      // write layout results to data stream
      const t = l.datum;
      t[as[0]] = l.x;
      t[as[1]] = l.y;
      t[as[2]] = l.opacity;
      t[as[3]] = l.align;
      t[as[4]] = l.baseline;
    });
    return pulse.reflow(mod).modifies(as);
  }
});

const label = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  label: Label$1
}, Symbol.toStringTag, { value: 'Module' }));

function partition (data, groupby) {
  var groups = [],
    get = function (f) {
      return f(t);
    },
    map,
    i,
    n,
    t,
    k,
    g;

  // partition data points into stack groups
  if (groupby == null) {
    groups.push(data);
  } else {
    for (map = {}, i = 0, n = data.length; i < n; ++i) {
      t = data[i];
      k = groupby.map(get);
      g = map[k];
      if (!g) {
        map[k] = g = [];
        g.dims = k;
        groups.push(g);
      }
      g.push(t);
    }
  }
  return groups;
}

/**
 * Compute locally-weighted regression fits for one or more data groups.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.x - An accessor for the predictor data field.
 * @param {function(object): *} params.y - An accessor for the predicted data field.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors to groupby.
 * @param {number} [params.bandwidth=0.3] - The loess bandwidth.
 */
function Loess(params) {
  Transform.call(this, null, params);
}
Loess.Definition = {
  'type': 'Loess',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'x',
    'type': 'field',
    'required': true
  }, {
    'name': 'y',
    'type': 'field',
    'required': true
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'bandwidth',
    'type': 'number',
    'default': 0.3
  }, {
    'name': 'as',
    'type': 'string',
    'array': true
  }]
};
inherits(Loess, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
    if (!this.value || pulse.changed() || _.modified()) {
      const source = pulse.materialize(pulse.SOURCE).source,
        groups = partition(source, _.groupby),
        names = (_.groupby || []).map(accessorName),
        m = names.length,
        as = _.as || [accessorName(_.x), accessorName(_.y)],
        values = [];
      groups.forEach(g => {
        loess(g, _.x, _.y, _.bandwidth || 0.3).forEach(p => {
          const t = {};
          for (let i = 0; i < m; ++i) {
            t[names[i]] = g.dims[i];
          }
          t[as[0]] = p[0];
          t[as[1]] = p[1];
          values.push(ingest$1(t));
        });
      });
      if (this.value) out.rem = this.value;
      this.value = out.add = out.source = values;
    }
    return out;
  }
});

const Methods = {
  constant: constant,
  linear: linear,
  log: log$1,
  exp: exp,
  pow: pow,
  quad: quad,
  poly: poly
};
const degreesOfFreedom = (method, order) => method === 'poly' ? order : method === 'quad' ? 2 : 1;

/**
 * Compute regression fits for one or more data groups.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {function(object): *} params.x - An accessor for the predictor data field.
 * @param {function(object): *} params.y - An accessor for the predicted data field.
 * @param {string} [params.method='linear'] - The regression method to apply.
 * @param {Array<function(object): *>} [params.groupby] - An array of accessors to groupby.
 * @param {Array<number>} [params.extent] - The domain extent over which to plot the regression line.
 * @param {number} [params.order=3] - The polynomial order. Only applies to the 'poly' method.
 */
function Regression(params) {
  Transform.call(this, null, params);
}
Regression.Definition = {
  'type': 'Regression',
  'metadata': {
    'generates': true
  },
  'params': [{
    'name': 'x',
    'type': 'field',
    'required': true
  }, {
    'name': 'y',
    'type': 'field',
    'required': true
  }, {
    'name': 'groupby',
    'type': 'field',
    'array': true
  }, {
    'name': 'method',
    'type': 'string',
    'default': 'linear',
    'values': Object.keys(Methods)
  }, {
    'name': 'order',
    'type': 'number',
    'default': 3
  }, {
    'name': 'extent',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'params',
    'type': 'boolean',
    'default': false
  }, {
    'name': 'as',
    'type': 'string',
    'array': true
  }]
};
inherits(Regression, Transform, {
  transform(_, pulse) {
    const out = pulse.fork(pulse.NO_SOURCE | pulse.NO_FIELDS);
    if (!this.value || pulse.changed() || _.modified()) {
      const source = pulse.materialize(pulse.SOURCE).source,
        groups = partition(source, _.groupby),
        names = (_.groupby || []).map(accessorName),
        method = _.method || 'linear',
        order = _.order == null ? 3 : _.order,
        dof = degreesOfFreedom(method, order),
        as = _.as || [accessorName(_.x), accessorName(_.y)],
        fit = Methods[method],
        values = [];
      let domain = _.extent;
      if (!has$1(Methods, method)) {
        error$1('Invalid regression method: ' + method);
      }
      if (domain != null) {
        if (method === 'log' && domain[0] <= 0) {
          pulse.dataflow.warn('Ignoring extent with values <= 0 for log regression.');
          domain = null;
        }
      }
      groups.forEach(g => {
        const n = g.length;
        if (n <= dof) {
          pulse.dataflow.warn('Skipping regression with more parameters than data points.');
          return;
        }
        const model = fit(g, _.x, _.y, order);
        if (_.params) {
          // if parameter vectors requested return those
          values.push(ingest$1({
            keys: g.dims,
            coef: model.coef,
            rSquared: model.rSquared
          }));
          return;
        }
        const dom = domain || extent(g, _.x),
          add = p => {
            const t = {};
            for (let i = 0; i < names.length; ++i) {
              t[names[i]] = g.dims[i];
            }
            t[as[0]] = p[0];
            t[as[1]] = p[1];
            values.push(ingest$1(t));
          };
        if (method === 'linear' || method === 'constant') {
          // for linear or constant regression we only need the end points
          dom.forEach(x => add([x, model.predict(x)]));
        } else {
          // otherwise return trend line sample points
          sampleCurve(model.predict, dom, 25, 200).forEach(add);
        }
      });
      if (this.value) out.rem = this.value;
      this.value = out.add = out.source = values;
    }
    return out;
  }
});

const reg = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  loess: Loess,
  regression: Regression
}, Symbol.toStringTag, { value: 'Module' }));

function Voronoi(params) {
  Transform.call(this, null, params);
}
Voronoi.Definition = {
  'type': 'Voronoi',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'x',
    'type': 'field',
    'required': true
  }, {
    'name': 'y',
    'type': 'field',
    'required': true
  }, {
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'extent',
    'type': 'array',
    'array': true,
    'length': 2,
    'default': [[-1e5, -1e5], [1e5, 1e5]],
    'content': {
      'type': 'number',
      'array': true,
      'length': 2
    }
  }, {
    'name': 'as',
    'type': 'string',
    'default': 'path'
  }]
};
const defaultExtent = [-1e5, -1e5, 1e5, 1e5];
inherits(Voronoi, Transform, {
  transform(_, pulse) {
    const as = _.as || 'path',
      data = pulse.source;

    // nothing to do if no data
    if (!data || !data.length) return pulse;

    // configure and construct voronoi diagram
    let s = _.size;
    s = s ? [0, 0, s[0], s[1]] : (s = _.extent) ? [s[0][0], s[0][1], s[1][0], s[1][1]] : defaultExtent;
    const voronoi = this.value = Delaunay.from(data, _.x, _.y).voronoi(s);

    // map polygons to paths
    for (let i = 0, n = data.length; i < n; ++i) {
      const polygon = voronoi.cellPolygon(i);
      data[i][as] = polygon && !isPoint(polygon) ? toPathString(polygon) : null;
    }
    return pulse.reflow(_.modified()).modifies(as);
  }
});

// suppress duplicated end point vertices
function toPathString(p) {
  const x = p[0][0],
    y = p[0][1];
  let n = p.length - 1;
  for (; p[n][0] === x && p[n][1] === y; --n);
  return 'M' + p.slice(0, n + 1).join('L') + 'Z';
}
function isPoint(p) {
  return p.length === 2 && p[0][0] === p[1][0] && p[0][1] === p[1][1];
}

const voronoi = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  voronoi: Voronoi
}, Symbol.toStringTag, { value: 'Module' }));

/*
Copyright (c) 2013, Jason Davies.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice, this
    list of conditions and the following disclaimer.

  * Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

  * The name Jason Davies may not be used to endorse or promote products
    derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL JASON DAVIES BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf

var cloudRadians = Math.PI / 180,
  cw = 1 << 11 >> 5,
  ch = 1 << 11;
function cloud () {
  var size = [256, 256],
    text,
    font,
    fontSize,
    fontStyle,
    fontWeight,
    rotate,
    padding,
    spiral = archimedeanSpiral,
    words = [],
    random = Math.random,
    cloud = {};
  cloud.layout = function () {
    var contextAndRatio = getContext(domCanvas()),
      board = zeroArray((size[0] >> 5) * size[1]),
      bounds = null,
      n = words.length,
      i = -1,
      tags = [],
      data = words.map(d => ({
        text: text(d),
        font: font(d),
        style: fontStyle(d),
        weight: fontWeight(d),
        rotate: rotate(d),
        size: ~~(fontSize(d) + 1e-14),
        padding: padding(d),
        xoff: 0,
        yoff: 0,
        x1: 0,
        y1: 0,
        x0: 0,
        y0: 0,
        hasText: false,
        sprite: null,
        datum: d
      })).sort((a, b) => b.size - a.size);
    while (++i < n) {
      var d = data[i];
      d.x = size[0] * (random() + .5) >> 1;
      d.y = size[1] * (random() + .5) >> 1;
      cloudSprite(contextAndRatio, d, data, i);
      if (d.hasText && place(board, d, bounds)) {
        tags.push(d);
        if (bounds) cloudBounds(bounds, d);else bounds = [{
          x: d.x + d.x0,
          y: d.y + d.y0
        }, {
          x: d.x + d.x1,
          y: d.y + d.y1
        }];
        // Temporary hack
        d.x -= size[0] >> 1;
        d.y -= size[1] >> 1;
      }
    }
    return tags;
  };
  function getContext(canvas) {
    canvas.width = canvas.height = 1;
    var ratio = Math.sqrt(canvas.getContext('2d').getImageData(0, 0, 1, 1).data.length >> 2);
    canvas.width = (cw << 5) / ratio;
    canvas.height = ch / ratio;
    var context = canvas.getContext('2d');
    context.fillStyle = context.strokeStyle = 'red';
    context.textAlign = 'center';
    return {
      context: context,
      ratio: ratio
    };
  }
  function place(board, tag, bounds) {
    var startX = tag.x,
      startY = tag.y,
      maxDelta = Math.hypot(size[0], size[1]),
      s = spiral(size),
      dt = random() < .5 ? 1 : -1,
      t = -dt,
      dxdy,
      dx,
      dy;
    while (dxdy = s(t += dt)) {
      dx = ~~dxdy[0];
      dy = ~~dxdy[1];
      if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
      tag.x = startX + dx;
      tag.y = startY + dy;
      if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
      // TODO only check for collisions within current bounds.
      if (!bounds || !cloudCollide(tag, board, size[0])) {
        if (!bounds || collideRects(tag, bounds)) {
          var sprite = tag.sprite,
            w = tag.width >> 5,
            sw = size[0] >> 5,
            lx = tag.x - (w << 4),
            sx = lx & 0x7f,
            msx = 32 - sx,
            h = tag.y1 - tag.y0,
            x = (tag.y + tag.y0) * sw + (lx >> 5),
            last;
          for (var j = 0; j < h; j++) {
            last = 0;
            for (var i = 0; i <= w; i++) {
              board[x + i] |= last << msx | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
            }
            x += sw;
          }
          tag.sprite = null;
          return true;
        }
      }
    }
    return false;
  }
  cloud.words = function (_) {
    if (arguments.length) {
      words = _;
      return cloud;
    } else {
      return words;
    }
  };
  cloud.size = function (_) {
    if (arguments.length) {
      size = [+_[0], +_[1]];
      return cloud;
    } else {
      return size;
    }
  };
  cloud.font = function (_) {
    if (arguments.length) {
      font = functor(_);
      return cloud;
    } else {
      return font;
    }
  };
  cloud.fontStyle = function (_) {
    if (arguments.length) {
      fontStyle = functor(_);
      return cloud;
    } else {
      return fontStyle;
    }
  };
  cloud.fontWeight = function (_) {
    if (arguments.length) {
      fontWeight = functor(_);
      return cloud;
    } else {
      return fontWeight;
    }
  };
  cloud.rotate = function (_) {
    if (arguments.length) {
      rotate = functor(_);
      return cloud;
    } else {
      return rotate;
    }
  };
  cloud.text = function (_) {
    if (arguments.length) {
      text = functor(_);
      return cloud;
    } else {
      return text;
    }
  };
  cloud.spiral = function (_) {
    if (arguments.length) {
      spiral = spirals[_] || _;
      return cloud;
    } else {
      return spiral;
    }
  };
  cloud.fontSize = function (_) {
    if (arguments.length) {
      fontSize = functor(_);
      return cloud;
    } else {
      return fontSize;
    }
  };
  cloud.padding = function (_) {
    if (arguments.length) {
      padding = functor(_);
      return cloud;
    } else {
      return padding;
    }
  };
  cloud.random = function (_) {
    if (arguments.length) {
      random = _;
      return cloud;
    } else {
      return random;
    }
  };
  return cloud;
}

// Fetches a monochrome sprite bitmap for the specified text.
// Load in batches for speed.
function cloudSprite(contextAndRatio, d, data, di) {
  if (d.sprite) return;
  var c = contextAndRatio.context,
    ratio = contextAndRatio.ratio;
  c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
  var x = 0,
    y = 0,
    maxh = 0,
    n = data.length,
    w,
    w32,
    h,
    i,
    j;
  --di;
  while (++di < n) {
    d = data[di];
    c.save();
    c.font = d.style + ' ' + d.weight + ' ' + ~~((d.size + 1) / ratio) + 'px ' + d.font;
    w = c.measureText(d.text + 'm').width * ratio;
    h = d.size << 1;
    if (d.rotate) {
      var sr = Math.sin(d.rotate * cloudRadians),
        cr = Math.cos(d.rotate * cloudRadians),
        wcr = w * cr,
        wsr = w * sr,
        hcr = h * cr,
        hsr = h * sr;
      w = Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f >> 5 << 5;
      h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
    } else {
      w = w + 0x1f >> 5 << 5;
    }
    if (h > maxh) maxh = h;
    if (x + w >= cw << 5) {
      x = 0;
      y += maxh;
      maxh = 0;
    }
    if (y + h >= ch) break;
    c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
    if (d.rotate) c.rotate(d.rotate * cloudRadians);
    c.fillText(d.text, 0, 0);
    if (d.padding) {
      c.lineWidth = 2 * d.padding;
      c.strokeText(d.text, 0, 0);
    }
    c.restore();
    d.width = w;
    d.height = h;
    d.xoff = x;
    d.yoff = y;
    d.x1 = w >> 1;
    d.y1 = h >> 1;
    d.x0 = -d.x1;
    d.y0 = -d.y1;
    d.hasText = true;
    x += w;
  }
  var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
    sprite = [];
  while (--di >= 0) {
    d = data[di];
    if (!d.hasText) continue;
    w = d.width;
    w32 = w >> 5;
    h = d.y1 - d.y0;
    // Zero the buffer
    for (i = 0; i < h * w32; i++) sprite[i] = 0;
    x = d.xoff;
    if (x == null) return;
    y = d.yoff;
    var seen = 0,
      seenRow = -1;
    for (j = 0; j < h; j++) {
      for (i = 0; i < w; i++) {
        var k = w32 * j + (i >> 5),
          m = pixels[(y + j) * (cw << 5) + (x + i) << 2] ? 1 << 31 - i % 32 : 0;
        sprite[k] |= m;
        seen |= m;
      }
      if (seen) seenRow = j;else {
        d.y0++;
        h--;
        j--;
        y++;
      }
    }
    d.y1 = d.y0 + seenRow;
    d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
  }
}

// Use mask-based collision detection.
function cloudCollide(tag, board, sw) {
  sw >>= 5;
  var sprite = tag.sprite,
    w = tag.width >> 5,
    lx = tag.x - (w << 4),
    sx = lx & 0x7f,
    msx = 32 - sx,
    h = tag.y1 - tag.y0,
    x = (tag.y + tag.y0) * sw + (lx >> 5),
    last;
  for (var j = 0; j < h; j++) {
    last = 0;
    for (var i = 0; i <= w; i++) {
      if ((last << msx | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) & board[x + i]) return true;
    }
    x += sw;
  }
  return false;
}
function cloudBounds(bounds, d) {
  var b0 = bounds[0],
    b1 = bounds[1];
  if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
  if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
  if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
  if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
}
function collideRects(a, b) {
  return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
}
function archimedeanSpiral(size) {
  var e = size[0] / size[1];
  return function (t) {
    return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
  };
}
function rectangularSpiral(size) {
  var dy = 4,
    dx = dy * size[0] / size[1],
    x = 0,
    y = 0;
  return function (t) {
    var sign = t < 0 ? -1 : 1;
    // See triangular numbers: T_n = n * (n + 1) / 2.
    switch (Math.sqrt(1 + 4 * sign * t) - sign & 3) {
      case 0:
        x += dx;
        break;
      case 1:
        y += dy;
        break;
      case 2:
        x -= dx;
        break;
      default:
        y -= dy;
        break;
    }
    return [x, y];
  };
}

// TODO reuse arrays?
function zeroArray(n) {
  var a = [],
    i = -1;
  while (++i < n) a[i] = 0;
  return a;
}
function functor(d) {
  return typeof d === 'function' ? d : function () {
    return d;
  };
}
var spirals = {
  archimedean: archimedeanSpiral,
  rectangular: rectangularSpiral
};

const Output = ['x', 'y', 'font', 'fontSize', 'fontStyle', 'fontWeight', 'angle'];
const Params$1 = ['text', 'font', 'rotate', 'fontSize', 'fontStyle', 'fontWeight'];
function Wordcloud(params) {
  Transform.call(this, cloud(), params);
}
Wordcloud.Definition = {
  'type': 'Wordcloud',
  'metadata': {
    'modifies': true
  },
  'params': [{
    'name': 'size',
    'type': 'number',
    'array': true,
    'length': 2
  }, {
    'name': 'font',
    'type': 'string',
    'expr': true,
    'default': 'sans-serif'
  }, {
    'name': 'fontStyle',
    'type': 'string',
    'expr': true,
    'default': 'normal'
  }, {
    'name': 'fontWeight',
    'type': 'string',
    'expr': true,
    'default': 'normal'
  }, {
    'name': 'fontSize',
    'type': 'number',
    'expr': true,
    'default': 14
  }, {
    'name': 'fontSizeRange',
    'type': 'number',
    'array': 'nullable',
    'default': [10, 50]
  }, {
    'name': 'rotate',
    'type': 'number',
    'expr': true,
    'default': 0
  }, {
    'name': 'text',
    'type': 'field'
  }, {
    'name': 'spiral',
    'type': 'string',
    'values': ['archimedean', 'rectangular']
  }, {
    'name': 'padding',
    'type': 'number',
    'expr': true
  }, {
    'name': 'as',
    'type': 'string',
    'array': true,
    'length': 7,
    'default': Output
  }]
};
inherits(Wordcloud, Transform, {
  transform(_, pulse) {
    if (_.size && !(_.size[0] && _.size[1])) {
      error$1('Wordcloud size dimensions must be non-zero.');
    }
    function modp(param) {
      const p = _[param];
      return isFunction(p) && pulse.modified(p.fields);
    }
    const mod = _.modified();
    if (!(mod || pulse.changed(pulse.ADD_REM) || Params$1.some(modp))) return;
    const data = pulse.materialize(pulse.SOURCE).source,
      layout = this.value,
      as = _.as || Output;
    let fontSize = _.fontSize || 14,
      range;
    isFunction(fontSize) ? range = _.fontSizeRange : fontSize = constant$1(fontSize);

    // create font size scaling function as needed
    if (range) {
      const fsize = fontSize,
        sizeScale = scale$4('sqrt')().domain(extent(data, fsize)).range(range);
      fontSize = x => sizeScale(fsize(x));
    }
    data.forEach(t => {
      t[as[0]] = NaN;
      t[as[1]] = NaN;
      t[as[3]] = 0;
    });

    // configure layout
    const words = layout.words(data).text(_.text).size(_.size || [500, 500]).padding(_.padding || 1).spiral(_.spiral || 'archimedean').rotate(_.rotate || 0).font(_.font || 'sans-serif').fontStyle(_.fontStyle || 'normal').fontWeight(_.fontWeight || 'normal').fontSize(fontSize).random(random).layout();
    const size = layout.size(),
      dx = size[0] >> 1,
      dy = size[1] >> 1,
      n = words.length;
    for (let i = 0, w, t; i < n; ++i) {
      w = words[i];
      t = w.datum;
      t[as[0]] = w.x + dx;
      t[as[1]] = w.y + dy;
      t[as[2]] = w.font;
      t[as[3]] = w.size;
      t[as[4]] = w.style;
      t[as[5]] = w.weight;
      t[as[6]] = w.rotate;
    }
    return pulse.reflow(mod).modifies(as);
  }
});

const wordcloud = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  wordcloud: Wordcloud
}, Symbol.toStringTag, { value: 'Module' }));

const array8 = n => new Uint8Array(n);
const array16 = n => new Uint16Array(n);
const array32 = n => new Uint32Array(n);

/**
 * Maintains CrossFilter state.
 */
function Bitmaps() {
  let width = 8,
    data = [],
    seen = array32(0),
    curr = array$1(0, width),
    prev = array$1(0, width);
  return {
    data: () => data,
    seen: () => seen = lengthen(seen, data.length),
    add(array) {
      for (let i = 0, j = data.length, n = array.length, t; i < n; ++i) {
        t = array[i];
        t._index = j++;
        data.push(t);
      }
    },
    remove(num, map) {
      // map: index -> boolean (true => remove)
      const n = data.length,
        copy = Array(n - num),
        reindex = data; // reuse old data array for index map
      let t, i, j;

      // seek forward to first removal
      for (i = 0; !map[i] && i < n; ++i) {
        copy[i] = data[i];
        reindex[i] = i;
      }

      // condense arrays
      for (j = i; i < n; ++i) {
        t = data[i];
        if (!map[i]) {
          reindex[i] = j;
          curr[j] = curr[i];
          prev[j] = prev[i];
          copy[j] = t;
          t._index = j++;
        } else {
          reindex[i] = -1;
        }
        curr[i] = 0; // clear unused bits
      }
      data = copy;
      return reindex;
    },
    size: () => data.length,
    curr: () => curr,
    prev: () => prev,
    reset: k => prev[k] = curr[k],
    all: () => width < 0x101 ? 0xff : width < 0x10001 ? 0xffff : 0xffffffff,
    set(k, one) {
      curr[k] |= one;
    },
    clear(k, one) {
      curr[k] &= ~one;
    },
    resize(n, m) {
      const k = curr.length;
      if (n > k || m > width) {
        width = Math.max(m, width);
        curr = array$1(n, width, curr);
        prev = array$1(n, width);
      }
    }
  };
}
function lengthen(array, length, copy) {
  if (array.length >= length) return array;
  copy = copy || new array.constructor(length);
  copy.set(array);
  return copy;
}
function array$1(n, m, array) {
  const copy = (m < 0x101 ? array8 : m < 0x10001 ? array16 : array32)(n);
  if (array) copy.set(array);
  return copy;
}

function Dimension (index, i, query) {
  const bit = 1 << i;
  return {
    one: bit,
    zero: ~bit,
    range: query.slice(),
    bisect: index.bisect,
    index: index.index,
    size: index.size,
    onAdd(added, curr) {
      const dim = this,
        range = dim.bisect(dim.range, added.value),
        idx = added.index,
        lo = range[0],
        hi = range[1],
        n1 = idx.length;
      let i;
      for (i = 0; i < lo; ++i) curr[idx[i]] |= bit;
      for (i = hi; i < n1; ++i) curr[idx[i]] |= bit;
      return dim;
    }
  };
}

/**
 * Maintains a list of values, sorted by key.
 */
function SortedIndex() {
  let index = array32(0),
    value = [],
    size = 0;
  function insert(key, data, base) {
    if (!data.length) return [];
    const n0 = size,
      n1 = data.length,
      addi = array32(n1);
    let addv = Array(n1),
      oldv,
      oldi,
      i;
    for (i = 0; i < n1; ++i) {
      addv[i] = key(data[i]);
      addi[i] = i;
    }
    addv = sort$1(addv, addi);
    if (n0) {
      oldv = value;
      oldi = index;
      value = Array(n0 + n1);
      index = array32(n0 + n1);
      merge$1(base, oldv, oldi, n0, addv, addi, n1, value, index);
    } else {
      if (base > 0) for (i = 0; i < n1; ++i) {
        addi[i] += base;
      }
      value = addv;
      index = addi;
    }
    size = n0 + n1;
    return {
      index: addi,
      value: addv
    };
  }
  function remove(num, map) {
    // map: index -> remove
    const n = size;
    let idx, i, j;

    // seek forward to first removal
    for (i = 0; !map[index[i]] && i < n; ++i);

    // condense index and value arrays
    for (j = i; i < n; ++i) {
      if (!map[idx = index[i]]) {
        index[j] = idx;
        value[j] = value[i];
        ++j;
      }
    }
    size = n - num;
  }
  function reindex(map) {
    for (let i = 0, n = size; i < n; ++i) {
      index[i] = map[index[i]];
    }
  }
  function bisect(range, array) {
    let n;
    if (array) {
      n = array.length;
    } else {
      array = value;
      n = size;
    }
    return [bisectLeft$1(array, range[0], 0, n), bisectRight$1(array, range[1], 0, n)];
  }
  return {
    insert: insert,
    remove: remove,
    bisect: bisect,
    reindex: reindex,
    index: () => index,
    size: () => size
  };
}
function sort$1(values, index) {
  values.sort.call(index, (a, b) => {
    const x = values[a],
      y = values[b];
    return x < y ? -1 : x > y ? 1 : 0;
  });
  return permute(values, index);
}
function merge$1(base, value0, index0, n0, value1, index1, n1, value, index) {
  let i0 = 0,
    i1 = 0,
    i;
  for (i = 0; i0 < n0 && i1 < n1; ++i) {
    if (value0[i0] < value1[i1]) {
      value[i] = value0[i0];
      index[i] = index0[i0++];
    } else {
      value[i] = value1[i1];
      index[i] = index1[i1++] + base;
    }
  }
  for (; i0 < n0; ++i0, ++i) {
    value[i] = value0[i0];
    index[i] = index0[i0];
  }
  for (; i1 < n1; ++i1, ++i) {
    value[i] = value1[i1];
    index[i] = index1[i1] + base;
  }
}

/**
 * An indexed multi-dimensional filter.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {Array<function(object): *>} params.fields - An array of dimension accessors to filter.
 * @param {Array} params.query - An array of per-dimension range queries.
 */
function CrossFilter(params) {
  Transform.call(this, Bitmaps(), params);
  this._indices = null;
  this._dims = null;
}
CrossFilter.Definition = {
  'type': 'CrossFilter',
  'metadata': {},
  'params': [{
    'name': 'fields',
    'type': 'field',
    'array': true,
    'required': true
  }, {
    'name': 'query',
    'type': 'array',
    'array': true,
    'required': true,
    'content': {
      'type': 'number',
      'array': true,
      'length': 2
    }
  }]
};
inherits(CrossFilter, Transform, {
  transform(_, pulse) {
    if (!this._dims) {
      return this.init(_, pulse);
    } else {
      var init = _.modified('fields') || _.fields.some(f => pulse.modified(f.fields));
      return init ? this.reinit(_, pulse) : this.eval(_, pulse);
    }
  },
  init(_, pulse) {
    const fields = _.fields,
      query = _.query,
      indices = this._indices = {},
      dims = this._dims = [],
      m = query.length;
    let i = 0,
      key,
      index;

    // instantiate indices and dimensions
    for (; i < m; ++i) {
      key = fields[i].fname;
      index = indices[key] || (indices[key] = SortedIndex());
      dims.push(Dimension(index, i, query[i]));
    }
    return this.eval(_, pulse);
  },
  reinit(_, pulse) {
    const output = pulse.materialize().fork(),
      fields = _.fields,
      query = _.query,
      indices = this._indices,
      dims = this._dims,
      bits = this.value,
      curr = bits.curr(),
      prev = bits.prev(),
      all = bits.all(),
      out = output.rem = output.add,
      mod = output.mod,
      m = query.length,
      adds = {};
    let add, index, key, mods, remMap, modMap, i, n, f;

    // set prev to current state
    prev.set(curr);

    // if pulse has remove tuples, process them first
    if (pulse.rem.length) {
      remMap = this.remove(_, pulse, output);
    }

    // if pulse has added tuples, add them to state
    if (pulse.add.length) {
      bits.add(pulse.add);
    }

    // if pulse has modified tuples, create an index map
    if (pulse.mod.length) {
      modMap = {};
      for (mods = pulse.mod, i = 0, n = mods.length; i < n; ++i) {
        modMap[mods[i]._index] = 1;
      }
    }

    // re-initialize indices as needed, update curr bitmap
    for (i = 0; i < m; ++i) {
      f = fields[i];
      if (!dims[i] || _.modified('fields', i) || pulse.modified(f.fields)) {
        key = f.fname;
        if (!(add = adds[key])) {
          indices[key] = index = SortedIndex();
          adds[key] = add = index.insert(f, pulse.source, 0);
        }
        dims[i] = Dimension(index, i, query[i]).onAdd(add, curr);
      }
    }

    // visit each tuple
    // if filter state changed, push index to add/rem
    // else if in mod and passes a filter, push index to mod
    for (i = 0, n = bits.data().length; i < n; ++i) {
      if (remMap[i]) {
        // skip if removed tuple
        continue;
      } else if (prev[i] !== curr[i]) {
        // add if state changed
        out.push(i);
      } else if (modMap[i] && curr[i] !== all) {
        // otherwise, pass mods through
        mod.push(i);
      }
    }
    bits.mask = (1 << m) - 1;
    return output;
  },
  eval(_, pulse) {
    const output = pulse.materialize().fork(),
      m = this._dims.length;
    let mask = 0;
    if (pulse.rem.length) {
      this.remove(_, pulse, output);
      mask |= (1 << m) - 1;
    }
    if (_.modified('query') && !_.modified('fields')) {
      mask |= this.update(_, pulse, output);
    }
    if (pulse.add.length) {
      this.insert(_, pulse, output);
      mask |= (1 << m) - 1;
    }
    if (pulse.mod.length) {
      this.modify(pulse, output);
      mask |= (1 << m) - 1;
    }
    this.value.mask = mask;
    return output;
  },
  insert(_, pulse, output) {
    const tuples = pulse.add,
      bits = this.value,
      dims = this._dims,
      indices = this._indices,
      fields = _.fields,
      adds = {},
      out = output.add,
      n = bits.size() + tuples.length,
      m = dims.length;
    let k = bits.size(),
      j,
      key,
      add;

    // resize bitmaps and add tuples as needed
    bits.resize(n, m);
    bits.add(tuples);
    const curr = bits.curr(),
      prev = bits.prev(),
      all = bits.all();

    // add to dimensional indices
    for (j = 0; j < m; ++j) {
      key = fields[j].fname;
      add = adds[key] || (adds[key] = indices[key].insert(fields[j], tuples, k));
      dims[j].onAdd(add, curr);
    }

    // set previous filters, output if passes at least one filter
    for (; k < n; ++k) {
      prev[k] = all;
      if (curr[k] !== all) out.push(k);
    }
  },
  modify(pulse, output) {
    const out = output.mod,
      bits = this.value,
      curr = bits.curr(),
      all = bits.all(),
      tuples = pulse.mod;
    let i, n, k;
    for (i = 0, n = tuples.length; i < n; ++i) {
      k = tuples[i]._index;
      if (curr[k] !== all) out.push(k);
    }
  },
  remove(_, pulse, output) {
    const indices = this._indices,
      bits = this.value,
      curr = bits.curr(),
      prev = bits.prev(),
      all = bits.all(),
      map = {},
      out = output.rem,
      tuples = pulse.rem;
    let i, n, k, f;

    // process tuples, output if passes at least one filter
    for (i = 0, n = tuples.length; i < n; ++i) {
      k = tuples[i]._index;
      map[k] = 1; // build index map
      prev[k] = f = curr[k];
      curr[k] = all;
      if (f !== all) out.push(k);
    }

    // remove from dimensional indices
    for (k in indices) {
      indices[k].remove(n, map);
    }
    this.reindex(pulse, n, map);
    return map;
  },
  // reindex filters and indices after propagation completes
  reindex(pulse, num, map) {
    const indices = this._indices,
      bits = this.value;
    pulse.runAfter(() => {
      const indexMap = bits.remove(num, map);
      for (const key in indices) indices[key].reindex(indexMap);
    });
  },
  update(_, pulse, output) {
    const dims = this._dims,
      query = _.query,
      stamp = pulse.stamp,
      m = dims.length;
    let mask = 0,
      i,
      q;

    // survey how many queries have changed
    output.filters = 0;
    for (q = 0; q < m; ++q) {
      if (_.modified('query', q)) {
        i = q;
        ++mask;
      }
    }
    if (mask === 1) {
      // only one query changed, use more efficient update
      mask = dims[i].one;
      this.incrementOne(dims[i], query[i], output.add, output.rem);
    } else {
      // multiple queries changed, perform full record keeping
      for (q = 0, mask = 0; q < m; ++q) {
        if (!_.modified('query', q)) continue;
        mask |= dims[q].one;
        this.incrementAll(dims[q], query[q], stamp, output.add);
        output.rem = output.add; // duplicate add/rem for downstream resolve
      }
    }
    return mask;
  },
  incrementAll(dim, query, stamp, out) {
    const bits = this.value,
      seen = bits.seen(),
      curr = bits.curr(),
      prev = bits.prev(),
      index = dim.index(),
      old = dim.bisect(dim.range),
      range = dim.bisect(query),
      lo1 = range[0],
      hi1 = range[1],
      lo0 = old[0],
      hi0 = old[1],
      one = dim.one;
    let i, j, k;

    // Fast incremental update based on previous lo index.
    if (lo1 < lo0) {
      for (i = lo1, j = Math.min(lo0, hi1); i < j; ++i) {
        k = index[i];
        if (seen[k] !== stamp) {
          prev[k] = curr[k];
          seen[k] = stamp;
          out.push(k);
        }
        curr[k] ^= one;
      }
    } else if (lo1 > lo0) {
      for (i = lo0, j = Math.min(lo1, hi0); i < j; ++i) {
        k = index[i];
        if (seen[k] !== stamp) {
          prev[k] = curr[k];
          seen[k] = stamp;
          out.push(k);
        }
        curr[k] ^= one;
      }
    }

    // Fast incremental update based on previous hi index.
    if (hi1 > hi0) {
      for (i = Math.max(lo1, hi0), j = hi1; i < j; ++i) {
        k = index[i];
        if (seen[k] !== stamp) {
          prev[k] = curr[k];
          seen[k] = stamp;
          out.push(k);
        }
        curr[k] ^= one;
      }
    } else if (hi1 < hi0) {
      for (i = Math.max(lo0, hi1), j = hi0; i < j; ++i) {
        k = index[i];
        if (seen[k] !== stamp) {
          prev[k] = curr[k];
          seen[k] = stamp;
          out.push(k);
        }
        curr[k] ^= one;
      }
    }
    dim.range = query.slice();
  },
  incrementOne(dim, query, add, rem) {
    const bits = this.value,
      curr = bits.curr(),
      index = dim.index(),
      old = dim.bisect(dim.range),
      range = dim.bisect(query),
      lo1 = range[0],
      hi1 = range[1],
      lo0 = old[0],
      hi0 = old[1],
      one = dim.one;
    let i, j, k;

    // Fast incremental update based on previous lo index.
    if (lo1 < lo0) {
      for (i = lo1, j = Math.min(lo0, hi1); i < j; ++i) {
        k = index[i];
        curr[k] ^= one;
        add.push(k);
      }
    } else if (lo1 > lo0) {
      for (i = lo0, j = Math.min(lo1, hi0); i < j; ++i) {
        k = index[i];
        curr[k] ^= one;
        rem.push(k);
      }
    }

    // Fast incremental update based on previous hi index.
    if (hi1 > hi0) {
      for (i = Math.max(lo1, hi0), j = hi1; i < j; ++i) {
        k = index[i];
        curr[k] ^= one;
        add.push(k);
      }
    } else if (hi1 < hi0) {
      for (i = Math.max(lo0, hi1), j = hi0; i < j; ++i) {
        k = index[i];
        curr[k] ^= one;
        rem.push(k);
      }
    }
    dim.range = query.slice();
  }
});

/**
 * Selectively filters tuples by resolving against a filter bitmap.
 * Useful for processing the output of a cross-filter transform.
 * @constructor
 * @param {object} params - The parameters for this operator.
 * @param {object} params.ignore - A bit mask indicating which filters to ignore.
 * @param {object} params.filter - The per-tuple filter bitmaps. Typically this
 *   parameter value is a reference to a {@link CrossFilter} transform.
 */
function ResolveFilter(params) {
  Transform.call(this, null, params);
}
ResolveFilter.Definition = {
  'type': 'ResolveFilter',
  'metadata': {},
  'params': [{
    'name': 'ignore',
    'type': 'number',
    'required': true,
    'description': 'A bit mask indicating which filters to ignore.'
  }, {
    'name': 'filter',
    'type': 'object',
    'required': true,
    'description': 'Per-tuple filter bitmaps from a CrossFilter transform.'
  }]
};
inherits(ResolveFilter, Transform, {
  transform(_, pulse) {
    const ignore = ~(_.ignore || 0),
      // bit mask where zeros -> dims to ignore
      bitmap = _.filter,
      mask = bitmap.mask;

    // exit early if no relevant filter changes
    if ((mask & ignore) === 0) return pulse.StopPropagation;
    const output = pulse.fork(pulse.ALL),
      data = bitmap.data(),
      curr = bitmap.curr(),
      prev = bitmap.prev(),
      pass = k => !(curr[k] & ignore) ? data[k] : null;

    // propagate all mod tuples that pass the filter
    output.filter(output.MOD, pass);

    // determine add & rem tuples via filter functions
    // for efficiency, we do *not* populate new arrays,
    // instead we add filter functions applied downstream

    if (!(mask & mask - 1)) {
      // only one filter changed
      output.filter(output.ADD, pass);
      output.filter(output.REM, k => (curr[k] & ignore) === mask ? data[k] : null);
    } else {
      // multiple filters changed
      output.filter(output.ADD, k => {
        const c = curr[k] & ignore,
          f = !c && c ^ prev[k] & ignore;
        return f ? data[k] : null;
      });
      output.filter(output.REM, k => {
        const c = curr[k] & ignore,
          f = c && !(c ^ (c ^ prev[k] & ignore));
        return f ? data[k] : null;
      });
    }

    // add filter to source data in case of reflow...
    return output.filter(output.SOURCE, t => pass(t._index));
  }
});

const xf = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  crossfilter: CrossFilter,
  resolvefilter: ResolveFilter
}, Symbol.toStringTag, { value: 'Module' }));

const RawCode = 'RawCode';
const Literal = 'Literal';
const Property = 'Property';
const Identifier = 'Identifier';
const ArrayExpression = 'ArrayExpression';
const BinaryExpression = 'BinaryExpression';
const CallExpression = 'CallExpression';
const ConditionalExpression = 'ConditionalExpression';
const LogicalExpression = 'LogicalExpression';
const MemberExpression = 'MemberExpression';
const ObjectExpression = 'ObjectExpression';
const UnaryExpression = 'UnaryExpression';
function ASTNode(type) {
  this.type = type;
}
ASTNode.prototype.visit = function (visitor) {
  let c, i, n;
  if (visitor(this)) return 1;
  for (c = children(this), i = 0, n = c.length; i < n; ++i) {
    if (c[i].visit(visitor)) return 1;
  }
};
function children(node) {
  switch (node.type) {
    case ArrayExpression:
      return node.elements;
    case BinaryExpression:
    case LogicalExpression:
      return [node.left, node.right];
    case CallExpression:
      return [node.callee].concat(node.arguments);
    case ConditionalExpression:
      return [node.test, node.consequent, node.alternate];
    case MemberExpression:
      return [node.object, node.property];
    case ObjectExpression:
      return node.properties;
    case Property:
      return [node.key, node.value];
    case UnaryExpression:
      return [node.argument];
    case Identifier:
    case Literal:
    case RawCode:
    default:
      return [];
  }
}

/*
  The following expression parser is based on Esprima (http://esprima.org/).
  Original header comment and license for Esprima is included here:

  Copyright (C) 2013 Ariya Hidayat <ariya.hidayat@gmail.com>
  Copyright (C) 2013 Thaddee Tyl <thaddee.tyl@gmail.com>
  Copyright (C) 2013 Mathias Bynens <mathias@qiwi.be>
  Copyright (C) 2012 Ariya Hidayat <ariya.hidayat@gmail.com>
  Copyright (C) 2012 Mathias Bynens <mathias@qiwi.be>
  Copyright (C) 2012 Joost-Wim Boekesteijn <joost-wim@boekesteijn.nl>
  Copyright (C) 2012 Kris Kowal <kris.kowal@cixar.com>
  Copyright (C) 2012 Yusuke Suzuki <utatane.tea@gmail.com>
  Copyright (C) 2012 Arpad Borsos <arpad.borsos@googlemail.com>
  Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var TokenName, source, index, length, lookahead;
var TokenBooleanLiteral = 1,
  TokenEOF = 2,
  TokenIdentifier = 3,
  TokenKeyword = 4,
  TokenNullLiteral = 5,
  TokenNumericLiteral = 6,
  TokenPunctuator = 7,
  TokenStringLiteral = 8,
  TokenRegularExpression = 9;
TokenName = {};
TokenName[TokenBooleanLiteral] = 'Boolean';
TokenName[TokenEOF] = '<end>';
TokenName[TokenIdentifier] = 'Identifier';
TokenName[TokenKeyword] = 'Keyword';
TokenName[TokenNullLiteral] = 'Null';
TokenName[TokenNumericLiteral] = 'Numeric';
TokenName[TokenPunctuator] = 'Punctuator';
TokenName[TokenStringLiteral] = 'String';
TokenName[TokenRegularExpression] = 'RegularExpression';
var SyntaxArrayExpression = 'ArrayExpression',
  SyntaxBinaryExpression = 'BinaryExpression',
  SyntaxCallExpression = 'CallExpression',
  SyntaxConditionalExpression = 'ConditionalExpression',
  SyntaxIdentifier = 'Identifier',
  SyntaxLiteral = 'Literal',
  SyntaxLogicalExpression = 'LogicalExpression',
  SyntaxMemberExpression = 'MemberExpression',
  SyntaxObjectExpression = 'ObjectExpression',
  SyntaxProperty = 'Property',
  SyntaxUnaryExpression = 'UnaryExpression';

// Error messages should be identical to V8.
var MessageUnexpectedToken = 'Unexpected token %0',
  MessageUnexpectedNumber = 'Unexpected number',
  MessageUnexpectedString = 'Unexpected string',
  MessageUnexpectedIdentifier = 'Unexpected identifier',
  MessageUnexpectedReserved = 'Unexpected reserved word',
  MessageUnexpectedEOS = 'Unexpected end of input',
  MessageInvalidRegExp = 'Invalid regular expression',
  MessageUnterminatedRegExp = 'Invalid regular expression: missing /',
  MessageStrictOctalLiteral = 'Octal literals are not allowed in strict mode.',
  MessageStrictDuplicateProperty = 'Duplicate data property in object literal not allowed in strict mode';
var ILLEGAL$1 = 'ILLEGAL',
  DISABLED = 'Disabled.';

// See also tools/generate-unicode-regex.py.
var RegexNonAsciiIdentifierStart = new RegExp('[\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0-\\u08B2\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F8\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA7AD\\uA7B0\\uA7B1\\uA7F7-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB5F\\uAB64\\uAB65\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]'),
  // eslint-disable-next-line no-misleading-character-class
  RegexNonAsciiIdentifierPart = new RegExp('[\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0300-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u0483-\\u0487\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0610-\\u061A\\u0620-\\u0669\\u066E-\\u06D3\\u06D5-\\u06DC\\u06DF-\\u06E8\\u06EA-\\u06FC\\u06FF\\u0710-\\u074A\\u074D-\\u07B1\\u07C0-\\u07F5\\u07FA\\u0800-\\u082D\\u0840-\\u085B\\u08A0-\\u08B2\\u08E4-\\u0963\\u0966-\\u096F\\u0971-\\u0983\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BC-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CE\\u09D7\\u09DC\\u09DD\\u09DF-\\u09E3\\u09E6-\\u09F1\\u0A01-\\u0A03\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A59-\\u0A5C\\u0A5E\\u0A66-\\u0A75\\u0A81-\\u0A83\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABC-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0AD0\\u0AE0-\\u0AE3\\u0AE6-\\u0AEF\\u0B01-\\u0B03\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3C-\\u0B44\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B5C\\u0B5D\\u0B5F-\\u0B63\\u0B66-\\u0B6F\\u0B71\\u0B82\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD0\\u0BD7\\u0BE6-\\u0BEF\\u0C00-\\u0C03\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C58\\u0C59\\u0C60-\\u0C63\\u0C66-\\u0C6F\\u0C81-\\u0C83\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBC-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0CDE\\u0CE0-\\u0CE3\\u0CE6-\\u0CEF\\u0CF1\\u0CF2\\u0D01-\\u0D03\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D-\\u0D44\\u0D46-\\u0D48\\u0D4A-\\u0D4E\\u0D57\\u0D60-\\u0D63\\u0D66-\\u0D6F\\u0D7A-\\u0D7F\\u0D82\\u0D83\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0DCA\\u0DCF-\\u0DD4\\u0DD6\\u0DD8-\\u0DDF\\u0DE6-\\u0DEF\\u0DF2\\u0DF3\\u0E01-\\u0E3A\\u0E40-\\u0E4E\\u0E50-\\u0E59\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB9\\u0EBB-\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EC8-\\u0ECD\\u0ED0-\\u0ED9\\u0EDC-\\u0EDF\\u0F00\\u0F18\\u0F19\\u0F20-\\u0F29\\u0F35\\u0F37\\u0F39\\u0F3E-\\u0F47\\u0F49-\\u0F6C\\u0F71-\\u0F84\\u0F86-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u1000-\\u1049\\u1050-\\u109D\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u135D-\\u135F\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F8\\u1700-\\u170C\\u170E-\\u1714\\u1720-\\u1734\\u1740-\\u1753\\u1760-\\u176C\\u176E-\\u1770\\u1772\\u1773\\u1780-\\u17D3\\u17D7\\u17DC\\u17DD\\u17E0-\\u17E9\\u180B-\\u180D\\u1810-\\u1819\\u1820-\\u1877\\u1880-\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1920-\\u192B\\u1930-\\u193B\\u1946-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u19D0-\\u19D9\\u1A00-\\u1A1B\\u1A20-\\u1A5E\\u1A60-\\u1A7C\\u1A7F-\\u1A89\\u1A90-\\u1A99\\u1AA7\\u1AB0-\\u1ABD\\u1B00-\\u1B4B\\u1B50-\\u1B59\\u1B6B-\\u1B73\\u1B80-\\u1BF3\\u1C00-\\u1C37\\u1C40-\\u1C49\\u1C4D-\\u1C7D\\u1CD0-\\u1CD2\\u1CD4-\\u1CF6\\u1CF8\\u1CF9\\u1D00-\\u1DF5\\u1DFC-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u200C\\u200D\\u203F\\u2040\\u2054\\u2071\\u207F\\u2090-\\u209C\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D7F-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2DE0-\\u2DFF\\u2E2F\\u3005-\\u3007\\u3021-\\u302F\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u3099\\u309A\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA62B\\uA640-\\uA66F\\uA674-\\uA67D\\uA67F-\\uA69D\\uA69F-\\uA6F1\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA7AD\\uA7B0\\uA7B1\\uA7F7-\\uA827\\uA840-\\uA873\\uA880-\\uA8C4\\uA8D0-\\uA8D9\\uA8E0-\\uA8F7\\uA8FB\\uA900-\\uA92D\\uA930-\\uA953\\uA960-\\uA97C\\uA980-\\uA9C0\\uA9CF-\\uA9D9\\uA9E0-\\uA9FE\\uAA00-\\uAA36\\uAA40-\\uAA4D\\uAA50-\\uAA59\\uAA60-\\uAA76\\uAA7A-\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEF\\uAAF2-\\uAAF6\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB5F\\uAB64\\uAB65\\uABC0-\\uABEA\\uABEC\\uABED\\uABF0-\\uABF9\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE00-\\uFE0F\\uFE20-\\uFE2D\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF10-\\uFF19\\uFF21-\\uFF3A\\uFF3F\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]');

// Ensure the condition is true, otherwise throw an error.
// This is only to have a better contract semantic, i.e. another safety net
// to catch a logic error. The condition shall be fulfilled in normal case.
// Do NOT use this to enforce a certain condition on any user input.

function assert(condition, message) {
  /* istanbul ignore next */
  if (!condition) {
    throw new Error('ASSERT: ' + message);
  }
}
function isDecimalDigit(ch) {
  return ch >= 0x30 && ch <= 0x39; // 0..9
}
function isHexDigit(ch) {
  return '0123456789abcdefABCDEF'.includes(ch);
}
function isOctalDigit(ch) {
  return '01234567'.includes(ch);
}

// 7.2 White Space

function isWhiteSpace(ch) {
  return ch === 0x20 || ch === 0x09 || ch === 0x0B || ch === 0x0C || ch === 0xA0 || ch >= 0x1680 && [0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF].includes(ch);
}

// 7.3 Line Terminators

function isLineTerminator(ch) {
  return ch === 0x0A || ch === 0x0D || ch === 0x2028 || ch === 0x2029;
}

// 7.6 Identifier Names and Identifiers

function isIdentifierStart(ch) {
  return ch === 0x24 || ch === 0x5F ||
  // $ (dollar) and _ (underscore)
  ch >= 0x41 && ch <= 0x5A ||
  // A..Z
  ch >= 0x61 && ch <= 0x7A ||
  // a..z
  ch === 0x5C ||
  // \ (backslash)
  ch >= 0x80 && RegexNonAsciiIdentifierStart.test(String.fromCharCode(ch));
}
function isIdentifierPart(ch) {
  return ch === 0x24 || ch === 0x5F ||
  // $ (dollar) and _ (underscore)
  ch >= 0x41 && ch <= 0x5A ||
  // A..Z
  ch >= 0x61 && ch <= 0x7A ||
  // a..z
  ch >= 0x30 && ch <= 0x39 ||
  // 0..9
  ch === 0x5C ||
  // \ (backslash)
  ch >= 0x80 && RegexNonAsciiIdentifierPart.test(String.fromCharCode(ch));
}

// 7.6.1.1 Keywords

const keywords = {
  'if': 1,
  'in': 1,
  'do': 1,
  'var': 1,
  'for': 1,
  'new': 1,
  'try': 1,
  'let': 1,
  'this': 1,
  'else': 1,
  'case': 1,
  'void': 1,
  'with': 1,
  'enum': 1,
  'while': 1,
  'break': 1,
  'catch': 1,
  'throw': 1,
  'const': 1,
  'yield': 1,
  'class': 1,
  'super': 1,
  'return': 1,
  'typeof': 1,
  'delete': 1,
  'switch': 1,
  'export': 1,
  'import': 1,
  'public': 1,
  'static': 1,
  'default': 1,
  'finally': 1,
  'extends': 1,
  'package': 1,
  'private': 1,
  'function': 1,
  'continue': 1,
  'debugger': 1,
  'interface': 1,
  'protected': 1,
  'instanceof': 1,
  'implements': 1
};
function skipComment() {
  while (index < length) {
    const ch = source.charCodeAt(index);
    if (isWhiteSpace(ch) || isLineTerminator(ch)) {
      ++index;
    } else {
      break;
    }
  }
}
function scanHexEscape(prefix) {
  var i,
    len,
    ch,
    code = 0;
  len = prefix === 'u' ? 4 : 2;
  for (i = 0; i < len; ++i) {
    if (index < length && isHexDigit(source[index])) {
      ch = source[index++];
      code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase());
    } else {
      throwError({}, MessageUnexpectedToken, ILLEGAL$1);
    }
  }
  return String.fromCharCode(code);
}
function scanUnicodeCodePointEscape() {
  var ch, code, cu1, cu2;
  ch = source[index];
  code = 0;

  // At least, one hex digit is required.
  if (ch === '}') {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }
  while (index < length) {
    ch = source[index++];
    if (!isHexDigit(ch)) {
      break;
    }
    code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase());
  }
  if (code > 0x10FFFF || ch !== '}') {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }

  // UTF-16 Encoding
  if (code <= 0xFFFF) {
    return String.fromCharCode(code);
  }
  cu1 = (code - 0x10000 >> 10) + 0xD800;
  cu2 = (code - 0x10000 & 1023) + 0xDC00;
  return String.fromCharCode(cu1, cu2);
}
function getEscapedIdentifier() {
  var ch, id;
  ch = source.charCodeAt(index++);
  id = String.fromCharCode(ch);

  // '\u' (U+005C, U+0075) denotes an escaped character.
  if (ch === 0x5C) {
    if (source.charCodeAt(index) !== 0x75) {
      throwError({}, MessageUnexpectedToken, ILLEGAL$1);
    }
    ++index;
    ch = scanHexEscape('u');
    if (!ch || ch === '\\' || !isIdentifierStart(ch.charCodeAt(0))) {
      throwError({}, MessageUnexpectedToken, ILLEGAL$1);
    }
    id = ch;
  }
  while (index < length) {
    ch = source.charCodeAt(index);
    if (!isIdentifierPart(ch)) {
      break;
    }
    ++index;
    id += String.fromCharCode(ch);

    // '\u' (U+005C, U+0075) denotes an escaped character.
    if (ch === 0x5C) {
      id = id.substr(0, id.length - 1);
      if (source.charCodeAt(index) !== 0x75) {
        throwError({}, MessageUnexpectedToken, ILLEGAL$1);
      }
      ++index;
      ch = scanHexEscape('u');
      if (!ch || ch === '\\' || !isIdentifierPart(ch.charCodeAt(0))) {
        throwError({}, MessageUnexpectedToken, ILLEGAL$1);
      }
      id += ch;
    }
  }
  return id;
}
function getIdentifier() {
  var start, ch;
  start = index++;
  while (index < length) {
    ch = source.charCodeAt(index);
    if (ch === 0x5C) {
      // Blackslash (U+005C) marks Unicode escape sequence.
      index = start;
      return getEscapedIdentifier();
    }
    if (isIdentifierPart(ch)) {
      ++index;
    } else {
      break;
    }
  }
  return source.slice(start, index);
}
function scanIdentifier() {
  var start, id, type;
  start = index;

  // Backslash (U+005C) starts an escaped character.
  id = source.charCodeAt(index) === 0x5C ? getEscapedIdentifier() : getIdentifier();

  // There is no keyword or literal with only one character.
  // Thus, it must be an identifier.
  if (id.length === 1) {
    type = TokenIdentifier;
  } else if (keywords.hasOwnProperty(id)) {
    // eslint-disable-line no-prototype-builtins
    type = TokenKeyword;
  } else if (id === 'null') {
    type = TokenNullLiteral;
  } else if (id === 'true' || id === 'false') {
    type = TokenBooleanLiteral;
  } else {
    type = TokenIdentifier;
  }
  return {
    type: type,
    value: id,
    start: start,
    end: index
  };
}

// 7.7 Punctuators

function scanPunctuator() {
  var start = index,
    code = source.charCodeAt(index),
    code2,
    ch1 = source[index],
    ch2,
    ch3,
    ch4;
  switch (code) {
    // Check for most common single-character punctuators.
    case 0x2E: // . dot
    case 0x28: // ( open bracket
    case 0x29: // ) close bracket
    case 0x3B: // ; semicolon
    case 0x2C: // , comma
    case 0x7B: // { open curly brace
    case 0x7D: // } close curly brace
    case 0x5B: // [
    case 0x5D: // ]
    case 0x3A: // :
    case 0x3F: // ?
    case 0x7E:
      // ~
      ++index;
      return {
        type: TokenPunctuator,
        value: String.fromCharCode(code),
        start: start,
        end: index
      };
    default:
      code2 = source.charCodeAt(index + 1);

      // '=' (U+003D) marks an assignment or comparison operator.
      if (code2 === 0x3D) {
        switch (code) {
          case 0x2B: // +
          case 0x2D: // -
          case 0x2F: // /
          case 0x3C: // <
          case 0x3E: // >
          case 0x5E: // ^
          case 0x7C: // |
          case 0x25: // %
          case 0x26: // &
          case 0x2A:
            // *
            index += 2;
            return {
              type: TokenPunctuator,
              value: String.fromCharCode(code) + String.fromCharCode(code2),
              start: start,
              end: index
            };
          case 0x21: // !
          case 0x3D:
            // =
            index += 2;

            // !== and ===
            if (source.charCodeAt(index) === 0x3D) {
              ++index;
            }
            return {
              type: TokenPunctuator,
              value: source.slice(start, index),
              start: start,
              end: index
            };
        }
      }
  }

  // 4-character punctuator: >>>=

  ch4 = source.substr(index, 4);
  if (ch4 === '>>>=') {
    index += 4;
    return {
      type: TokenPunctuator,
      value: ch4,
      start: start,
      end: index
    };
  }

  // 3-character punctuators: === !== >>> <<= >>=

  ch3 = ch4.substr(0, 3);
  if (ch3 === '>>>' || ch3 === '<<=' || ch3 === '>>=') {
    index += 3;
    return {
      type: TokenPunctuator,
      value: ch3,
      start: start,
      end: index
    };
  }

  // Other 2-character punctuators: ++ -- << >> && ||
  ch2 = ch3.substr(0, 2);
  if (ch1 === ch2[1] && '+-<>&|'.includes(ch1) || ch2 === '=>') {
    index += 2;
    return {
      type: TokenPunctuator,
      value: ch2,
      start: start,
      end: index
    };
  }
  if (ch2 === '//') {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }

  // 1-character punctuators: < > = ! + - * % & | ^ /

  if ('<>=!+-*%&|^/'.includes(ch1)) {
    ++index;
    return {
      type: TokenPunctuator,
      value: ch1,
      start: start,
      end: index
    };
  }
  throwError({}, MessageUnexpectedToken, ILLEGAL$1);
}

// 7.8.3 Numeric Literals

function scanHexLiteral(start) {
  let number = '';
  while (index < length) {
    if (!isHexDigit(source[index])) {
      break;
    }
    number += source[index++];
  }
  if (number.length === 0) {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }
  if (isIdentifierStart(source.charCodeAt(index))) {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }
  return {
    type: TokenNumericLiteral,
    value: parseInt('0x' + number, 16),
    start: start,
    end: index
  };
}
function scanOctalLiteral(start) {
  let number = '0' + source[index++];
  while (index < length) {
    if (!isOctalDigit(source[index])) {
      break;
    }
    number += source[index++];
  }
  if (isIdentifierStart(source.charCodeAt(index)) || isDecimalDigit(source.charCodeAt(index))) {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }
  return {
    type: TokenNumericLiteral,
    value: parseInt(number, 8),
    octal: true,
    start: start,
    end: index
  };
}
function scanNumericLiteral() {
  var number, start, ch;
  ch = source[index];
  assert(isDecimalDigit(ch.charCodeAt(0)) || ch === '.', 'Numeric literal must start with a decimal digit or a decimal point');
  start = index;
  number = '';
  if (ch !== '.') {
    number = source[index++];
    ch = source[index];

    // Hex number starts with '0x'.
    // Octal number starts with '0'.
    if (number === '0') {
      if (ch === 'x' || ch === 'X') {
        ++index;
        return scanHexLiteral(start);
      }
      if (isOctalDigit(ch)) {
        return scanOctalLiteral(start);
      }

      // decimal number starts with '0' such as '09' is illegal.
      if (ch && isDecimalDigit(ch.charCodeAt(0))) {
        throwError({}, MessageUnexpectedToken, ILLEGAL$1);
      }
    }
    while (isDecimalDigit(source.charCodeAt(index))) {
      number += source[index++];
    }
    ch = source[index];
  }
  if (ch === '.') {
    number += source[index++];
    while (isDecimalDigit(source.charCodeAt(index))) {
      number += source[index++];
    }
    ch = source[index];
  }
  if (ch === 'e' || ch === 'E') {
    number += source[index++];
    ch = source[index];
    if (ch === '+' || ch === '-') {
      number += source[index++];
    }
    if (isDecimalDigit(source.charCodeAt(index))) {
      while (isDecimalDigit(source.charCodeAt(index))) {
        number += source[index++];
      }
    } else {
      throwError({}, MessageUnexpectedToken, ILLEGAL$1);
    }
  }
  if (isIdentifierStart(source.charCodeAt(index))) {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }
  return {
    type: TokenNumericLiteral,
    value: parseFloat(number),
    start: start,
    end: index
  };
}

// 7.8.4 String Literals

function scanStringLiteral() {
  var str = '',
    quote,
    start,
    ch,
    code,
    octal = false;
  quote = source[index];
  assert(quote === '\'' || quote === '"', 'String literal must starts with a quote');
  start = index;
  ++index;
  while (index < length) {
    ch = source[index++];
    if (ch === quote) {
      quote = '';
      break;
    } else if (ch === '\\') {
      ch = source[index++];
      if (!ch || !isLineTerminator(ch.charCodeAt(0))) {
        switch (ch) {
          case 'u':
          case 'x':
            if (source[index] === '{') {
              ++index;
              str += scanUnicodeCodePointEscape();
            } else {
              str += scanHexEscape(ch);
            }
            break;
          case 'n':
            str += '\n';
            break;
          case 'r':
            str += '\r';
            break;
          case 't':
            str += '\t';
            break;
          case 'b':
            str += '\b';
            break;
          case 'f':
            str += '\f';
            break;
          case 'v':
            str += '\x0B';
            break;
          default:
            if (isOctalDigit(ch)) {
              code = '01234567'.indexOf(ch);

              // \0 is not octal escape sequence
              if (code !== 0) {
                octal = true;
              }
              if (index < length && isOctalDigit(source[index])) {
                octal = true;
                code = code * 8 + '01234567'.indexOf(source[index++]);

                // 3 digits are only allowed when string starts
                // with 0, 1, 2, 3
                if ('0123'.includes(ch) && index < length && isOctalDigit(source[index])) {
                  code = code * 8 + '01234567'.indexOf(source[index++]);
                }
              }
              str += String.fromCharCode(code);
            } else {
              str += ch;
            }
            break;
        }
      } else {
        if (ch === '\r' && source[index] === '\n') {
          ++index;
        }
      }
    } else if (isLineTerminator(ch.charCodeAt(0))) {
      break;
    } else {
      str += ch;
    }
  }
  if (quote !== '') {
    throwError({}, MessageUnexpectedToken, ILLEGAL$1);
  }
  return {
    type: TokenStringLiteral,
    value: str,
    octal: octal,
    start: start,
    end: index
  };
}
function testRegExp(pattern, flags) {
  let tmp = pattern;
  if (flags.includes('u')) {
    // Replace each astral symbol and every Unicode code point
    // escape sequence with a single ASCII symbol to avoid throwing on
    // regular expressions that are only valid in combination with the
    // `/u` flag.
    // Note: replacing with the ASCII symbol `x` might cause false
    // negatives in unlikely scenarios. For example, `[\u{61}-b]` is a
    // perfectly valid pattern that is equivalent to `[a-b]`, but it
    // would be replaced by `[x-b]` which throws an error.
    tmp = tmp.replace(/\\u\{([0-9a-fA-F]+)\}/g, ($0, $1) => {
      if (parseInt($1, 16) <= 0x10FFFF) {
        return 'x';
      }
      throwError({}, MessageInvalidRegExp);
    }).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 'x');
  }

  // First, detect invalid regular expressions.
  try {
    new RegExp(tmp);
  } catch (e) {
    throwError({}, MessageInvalidRegExp);
  }

  // Return a regular expression object for this pattern-flag pair, or
  // `null` in case the current environment doesn't support the flags it
  // uses.
  try {
    return new RegExp(pattern, flags);
  } catch (exception) {
    return null;
  }
}
function scanRegExpBody() {
  var ch, str, classMarker, terminated, body;
  ch = source[index];
  assert(ch === '/', 'Regular expression literal must start with a slash');
  str = source[index++];
  classMarker = false;
  terminated = false;
  while (index < length) {
    ch = source[index++];
    str += ch;
    if (ch === '\\') {
      ch = source[index++];
      // ECMA-262 7.8.5
      if (isLineTerminator(ch.charCodeAt(0))) {
        throwError({}, MessageUnterminatedRegExp);
      }
      str += ch;
    } else if (isLineTerminator(ch.charCodeAt(0))) {
      throwError({}, MessageUnterminatedRegExp);
    } else if (classMarker) {
      if (ch === ']') {
        classMarker = false;
      }
    } else {
      if (ch === '/') {
        terminated = true;
        break;
      } else if (ch === '[') {
        classMarker = true;
      }
    }
  }
  if (!terminated) {
    throwError({}, MessageUnterminatedRegExp);
  }

  // Exclude leading and trailing slash.
  body = str.substr(1, str.length - 2);
  return {
    value: body,
    literal: str
  };
}
function scanRegExpFlags() {
  var ch, str, flags;
  str = '';
  flags = '';
  while (index < length) {
    ch = source[index];
    if (!isIdentifierPart(ch.charCodeAt(0))) {
      break;
    }
    ++index;
    if (ch === '\\' && index < length) {
      throwError({}, MessageUnexpectedToken, ILLEGAL$1);
    } else {
      flags += ch;
      str += ch;
    }
  }
  if (flags.search(/[^gimuy]/g) >= 0) {
    throwError({}, MessageInvalidRegExp, flags);
  }
  return {
    value: flags,
    literal: str
  };
}
function scanRegExp() {
  var start, body, flags, value;
  lookahead = null;
  skipComment();
  start = index;
  body = scanRegExpBody();
  flags = scanRegExpFlags();
  value = testRegExp(body.value, flags.value);
  return {
    literal: body.literal + flags.literal,
    value: value,
    regex: {
      pattern: body.value,
      flags: flags.value
    },
    start: start,
    end: index
  };
}
function isIdentifierName(token) {
  return token.type === TokenIdentifier || token.type === TokenKeyword || token.type === TokenBooleanLiteral || token.type === TokenNullLiteral;
}
function advance() {
  skipComment();
  if (index >= length) {
    return {
      type: TokenEOF,
      start: index,
      end: index
    };
  }
  const ch = source.charCodeAt(index);
  if (isIdentifierStart(ch)) {
    return scanIdentifier();
  }

  // Very common: ( and ) and ;
  if (ch === 0x28 || ch === 0x29 || ch === 0x3B) {
    return scanPunctuator();
  }

  // String literal starts with single quote (U+0027) or double quote (U+0022).
  if (ch === 0x27 || ch === 0x22) {
    return scanStringLiteral();
  }

  // Dot (.) U+002E can also start a floating-point number, hence the need
  // to check the next character.
  if (ch === 0x2E) {
    if (isDecimalDigit(source.charCodeAt(index + 1))) {
      return scanNumericLiteral();
    }
    return scanPunctuator();
  }
  if (isDecimalDigit(ch)) {
    return scanNumericLiteral();
  }
  return scanPunctuator();
}
function lex() {
  const token = lookahead;
  index = token.end;
  lookahead = advance();
  index = token.end;
  return token;
}
function peek() {
  const pos = index;
  lookahead = advance();
  index = pos;
}
function finishArrayExpression(elements) {
  const node = new ASTNode(SyntaxArrayExpression);
  node.elements = elements;
  return node;
}
function finishBinaryExpression(operator, left, right) {
  const node = new ASTNode(operator === '||' || operator === '&&' ? SyntaxLogicalExpression : SyntaxBinaryExpression);
  node.operator = operator;
  node.left = left;
  node.right = right;
  return node;
}
function finishCallExpression(callee, args) {
  const node = new ASTNode(SyntaxCallExpression);
  node.callee = callee;
  node.arguments = args;
  return node;
}
function finishConditionalExpression(test, consequent, alternate) {
  const node = new ASTNode(SyntaxConditionalExpression);
  node.test = test;
  node.consequent = consequent;
  node.alternate = alternate;
  return node;
}
function finishIdentifier(name) {
  const node = new ASTNode(SyntaxIdentifier);
  node.name = name;
  return node;
}
function finishLiteral(token) {
  const node = new ASTNode(SyntaxLiteral);
  node.value = token.value;
  node.raw = source.slice(token.start, token.end);
  if (token.regex) {
    if (node.raw === '//') {
      node.raw = '/(?:)/';
    }
    node.regex = token.regex;
  }
  return node;
}
function finishMemberExpression(accessor, object, property) {
  const node = new ASTNode(SyntaxMemberExpression);
  node.computed = accessor === '[';
  node.object = object;
  node.property = property;
  if (!node.computed) property.member = true;
  return node;
}
function finishObjectExpression(properties) {
  const node = new ASTNode(SyntaxObjectExpression);
  node.properties = properties;
  return node;
}
function finishProperty(kind, key, value) {
  const node = new ASTNode(SyntaxProperty);
  node.key = key;
  node.value = value;
  node.kind = kind;
  return node;
}
function finishUnaryExpression(operator, argument) {
  const node = new ASTNode(SyntaxUnaryExpression);
  node.operator = operator;
  node.argument = argument;
  node.prefix = true;
  return node;
}

// Throw an exception

function throwError(token, messageFormat) {
  var error,
    args = Array.prototype.slice.call(arguments, 2),
    msg = messageFormat.replace(/%(\d)/g, (whole, index) => {
      assert(index < args.length, 'Message reference must be in range');
      return args[index];
    });
  error = new Error(msg);
  error.index = index;
  error.description = msg;
  throw error;
}

// Throw an exception because of the token.

function throwUnexpected(token) {
  if (token.type === TokenEOF) {
    throwError(token, MessageUnexpectedEOS);
  }
  if (token.type === TokenNumericLiteral) {
    throwError(token, MessageUnexpectedNumber);
  }
  if (token.type === TokenStringLiteral) {
    throwError(token, MessageUnexpectedString);
  }
  if (token.type === TokenIdentifier) {
    throwError(token, MessageUnexpectedIdentifier);
  }
  if (token.type === TokenKeyword) {
    throwError(token, MessageUnexpectedReserved);
  }

  // BooleanLiteral, NullLiteral, or Punctuator.
  throwError(token, MessageUnexpectedToken, token.value);
}

// Expect the next token to match the specified punctuator.
// If not, an exception will be thrown.

function expect(value) {
  const token = lex();
  if (token.type !== TokenPunctuator || token.value !== value) {
    throwUnexpected(token);
  }
}

// Return true if the next token matches the specified punctuator.

function match(value) {
  return lookahead.type === TokenPunctuator && lookahead.value === value;
}

// Return true if the next token matches the specified keyword

function matchKeyword(keyword) {
  return lookahead.type === TokenKeyword && lookahead.value === keyword;
}

// 11.1.4 Array Initialiser

function parseArrayInitialiser() {
  const elements = [];
  index = lookahead.start;
  expect('[');
  while (!match(']')) {
    if (match(',')) {
      lex();
      elements.push(null);
    } else {
      elements.push(parseConditionalExpression());
      if (!match(']')) {
        expect(',');
      }
    }
  }
  lex();
  return finishArrayExpression(elements);
}

// 11.1.5 Object Initialiser

function parseObjectPropertyKey() {
  index = lookahead.start;
  const token = lex();

  // Note: This function is called only from parseObjectProperty(), where
  // EOF and Punctuator tokens are already filtered out.

  if (token.type === TokenStringLiteral || token.type === TokenNumericLiteral) {
    if (token.octal) {
      throwError(token, MessageStrictOctalLiteral);
    }
    return finishLiteral(token);
  }
  return finishIdentifier(token.value);
}
function parseObjectProperty() {
  var token, key, id, value;
  index = lookahead.start;
  token = lookahead;
  if (token.type === TokenIdentifier) {
    id = parseObjectPropertyKey();
    expect(':');
    value = parseConditionalExpression();
    return finishProperty('init', id, value);
  }
  if (token.type === TokenEOF || token.type === TokenPunctuator) {
    throwUnexpected(token);
  } else {
    key = parseObjectPropertyKey();
    expect(':');
    value = parseConditionalExpression();
    return finishProperty('init', key, value);
  }
}
function parseObjectInitialiser() {
  var properties = [],
    property,
    name,
    key,
    map = {},
    toString = String;
  index = lookahead.start;
  expect('{');
  while (!match('}')) {
    property = parseObjectProperty();
    if (property.key.type === SyntaxIdentifier) {
      name = property.key.name;
    } else {
      name = toString(property.key.value);
    }
    key = '$' + name;
    if (Object.prototype.hasOwnProperty.call(map, key)) {
      throwError({}, MessageStrictDuplicateProperty);
    } else {
      map[key] = true;
    }
    properties.push(property);
    if (!match('}')) {
      expect(',');
    }
  }
  expect('}');
  return finishObjectExpression(properties);
}

// 11.1.6 The Grouping Operator

function parseGroupExpression() {
  expect('(');
  const expr = parseExpression$1();
  expect(')');
  return expr;
}

// 11.1 Primary Expressions

const legalKeywords = {
  'if': 1
};
function parsePrimaryExpression() {
  var type, token, expr;
  if (match('(')) {
    return parseGroupExpression();
  }
  if (match('[')) {
    return parseArrayInitialiser();
  }
  if (match('{')) {
    return parseObjectInitialiser();
  }
  type = lookahead.type;
  index = lookahead.start;
  if (type === TokenIdentifier || legalKeywords[lookahead.value]) {
    expr = finishIdentifier(lex().value);
  } else if (type === TokenStringLiteral || type === TokenNumericLiteral) {
    if (lookahead.octal) {
      throwError(lookahead, MessageStrictOctalLiteral);
    }
    expr = finishLiteral(lex());
  } else if (type === TokenKeyword) {
    throw new Error(DISABLED);
  } else if (type === TokenBooleanLiteral) {
    token = lex();
    token.value = token.value === 'true';
    expr = finishLiteral(token);
  } else if (type === TokenNullLiteral) {
    token = lex();
    token.value = null;
    expr = finishLiteral(token);
  } else if (match('/') || match('/=')) {
    expr = finishLiteral(scanRegExp());
    peek();
  } else {
    throwUnexpected(lex());
  }
  return expr;
}

// 11.2 Left-Hand-Side Expressions

function parseArguments() {
  const args = [];
  expect('(');
  if (!match(')')) {
    while (index < length) {
      args.push(parseConditionalExpression());
      if (match(')')) {
        break;
      }
      expect(',');
    }
  }
  expect(')');
  return args;
}
function parseNonComputedProperty() {
  index = lookahead.start;
  const token = lex();
  if (!isIdentifierName(token)) {
    throwUnexpected(token);
  }
  return finishIdentifier(token.value);
}
function parseNonComputedMember() {
  expect('.');
  return parseNonComputedProperty();
}
function parseComputedMember() {
  expect('[');
  const expr = parseExpression$1();
  expect(']');
  return expr;
}
function parseLeftHandSideExpressionAllowCall() {
  var expr, args, property;
  expr = parsePrimaryExpression();
  for (;;) {
    if (match('.')) {
      property = parseNonComputedMember();
      expr = finishMemberExpression('.', expr, property);
    } else if (match('(')) {
      args = parseArguments();
      expr = finishCallExpression(expr, args);
    } else if (match('[')) {
      property = parseComputedMember();
      expr = finishMemberExpression('[', expr, property);
    } else {
      break;
    }
  }
  return expr;
}

// 11.3 Postfix Expressions

function parsePostfixExpression() {
  const expr = parseLeftHandSideExpressionAllowCall();
  if (lookahead.type === TokenPunctuator) {
    if (match('++') || match('--')) {
      throw new Error(DISABLED);
    }
  }
  return expr;
}

// 11.4 Unary Operators

function parseUnaryExpression() {
  var token, expr;
  if (lookahead.type !== TokenPunctuator && lookahead.type !== TokenKeyword) {
    expr = parsePostfixExpression();
  } else if (match('++') || match('--')) {
    throw new Error(DISABLED);
  } else if (match('+') || match('-') || match('~') || match('!')) {
    token = lex();
    expr = parseUnaryExpression();
    expr = finishUnaryExpression(token.value, expr);
  } else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) {
    throw new Error(DISABLED);
  } else {
    expr = parsePostfixExpression();
  }
  return expr;
}
function binaryPrecedence(token) {
  let prec = 0;
  if (token.type !== TokenPunctuator && token.type !== TokenKeyword) {
    return 0;
  }
  switch (token.value) {
    case '||':
      prec = 1;
      break;
    case '&&':
      prec = 2;
      break;
    case '|':
      prec = 3;
      break;
    case '^':
      prec = 4;
      break;
    case '&':
      prec = 5;
      break;
    case '==':
    case '!=':
    case '===':
    case '!==':
      prec = 6;
      break;
    case '<':
    case '>':
    case '<=':
    case '>=':
    case 'instanceof':
    case 'in':
      prec = 7;
      break;
    case '<<':
    case '>>':
    case '>>>':
      prec = 8;
      break;
    case '+':
    case '-':
      prec = 9;
      break;
    case '*':
    case '/':
    case '%':
      prec = 11;
      break;
  }
  return prec;
}

// 11.5 Multiplicative Operators
// 11.6 Additive Operators
// 11.7 Bitwise Shift Operators
// 11.8 Relational Operators
// 11.9 Equality Operators
// 11.10 Binary Bitwise Operators
// 11.11 Binary Logical Operators

function parseBinaryExpression() {
  var marker, markers, expr, token, prec, stack, right, operator, left, i;
  marker = lookahead;
  left = parseUnaryExpression();
  token = lookahead;
  prec = binaryPrecedence(token);
  if (prec === 0) {
    return left;
  }
  token.prec = prec;
  lex();
  markers = [marker, lookahead];
  right = parseUnaryExpression();
  stack = [left, token, right];
  while ((prec = binaryPrecedence(lookahead)) > 0) {
    // Reduce: make a binary expression from the three topmost entries.
    while (stack.length > 2 && prec <= stack[stack.length - 2].prec) {
      right = stack.pop();
      operator = stack.pop().value;
      left = stack.pop();
      markers.pop();
      expr = finishBinaryExpression(operator, left, right);
      stack.push(expr);
    }

    // Shift.
    token = lex();
    token.prec = prec;
    stack.push(token);
    markers.push(lookahead);
    expr = parseUnaryExpression();
    stack.push(expr);
  }

  // Final reduce to clean-up the stack.
  i = stack.length - 1;
  expr = stack[i];
  markers.pop();
  while (i > 1) {
    markers.pop();
    expr = finishBinaryExpression(stack[i - 1].value, stack[i - 2], expr);
    i -= 2;
  }
  return expr;
}

// 11.12 Conditional Operator

function parseConditionalExpression() {
  var expr, consequent, alternate;
  expr = parseBinaryExpression();
  if (match('?')) {
    lex();
    consequent = parseConditionalExpression();
    expect(':');
    alternate = parseConditionalExpression();
    expr = finishConditionalExpression(expr, consequent, alternate);
  }
  return expr;
}

// 11.14 Comma Operator

function parseExpression$1() {
  const expr = parseConditionalExpression();
  if (match(',')) {
    throw new Error(DISABLED); // no sequence expressions
  }
  return expr;
}
function parser$1 (code) {
  source = code;
  index = 0;
  length = source.length;
  lookahead = null;
  peek();
  const expr = parseExpression$1();
  if (lookahead.type !== TokenEOF) {
    throw new Error('Unexpect token after expression.');
  }
  return expr;
}

var Constants$1 = {
  NaN: 'NaN',
  E: 'Math.E',
  LN2: 'Math.LN2',
  LN10: 'Math.LN10',
  LOG2E: 'Math.LOG2E',
  LOG10E: 'Math.LOG10E',
  PI: 'Math.PI',
  SQRT1_2: 'Math.SQRT1_2',
  SQRT2: 'Math.SQRT2',
  MIN_VALUE: 'Number.MIN_VALUE',
  MAX_VALUE: 'Number.MAX_VALUE'
};

function Functions$1 (codegen) {
  function fncall(name, args, cast, type) {
    let obj = codegen(args[0]);
    if (cast) {
      obj = cast + '(' + obj + ')';
      if (cast.lastIndexOf('new ', 0) === 0) obj = '(' + obj + ')';
    }
    return obj + '.' + name + (type < 0 ? '' : type === 0 ? '()' : '(' + args.slice(1).map(codegen).join(',') + ')');
  }
  function fn(name, cast, type) {
    return args => fncall(name, args, cast, type);
  }
  const DATE = 'new Date',
    STRING = 'String',
    REGEXP = 'RegExp';
  return {
    // MATH functions
    isNaN: 'Number.isNaN',
    isFinite: 'Number.isFinite',
    abs: 'Math.abs',
    acos: 'Math.acos',
    asin: 'Math.asin',
    atan: 'Math.atan',
    atan2: 'Math.atan2',
    ceil: 'Math.ceil',
    cos: 'Math.cos',
    exp: 'Math.exp',
    floor: 'Math.floor',
    hypot: 'Math.hypot',
    log: 'Math.log',
    max: 'Math.max',
    min: 'Math.min',
    pow: 'Math.pow',
    random: 'Math.random',
    round: 'Math.round',
    sin: 'Math.sin',
    sqrt: 'Math.sqrt',
    tan: 'Math.tan',
    clamp: function (args) {
      if (args.length < 3) error$1('Missing arguments to clamp function.');
      if (args.length > 3) error$1('Too many arguments to clamp function.');
      const a = args.map(codegen);
      return 'Math.max(' + a[1] + ', Math.min(' + a[2] + ',' + a[0] + '))';
    },
    // DATE functions
    now: 'Date.now',
    utc: 'Date.UTC',
    datetime: DATE,
    date: fn('getDate', DATE, 0),
    day: fn('getDay', DATE, 0),
    year: fn('getFullYear', DATE, 0),
    month: fn('getMonth', DATE, 0),
    hours: fn('getHours', DATE, 0),
    minutes: fn('getMinutes', DATE, 0),
    seconds: fn('getSeconds', DATE, 0),
    milliseconds: fn('getMilliseconds', DATE, 0),
    time: fn('getTime', DATE, 0),
    timezoneoffset: fn('getTimezoneOffset', DATE, 0),
    utcdate: fn('getUTCDate', DATE, 0),
    utcday: fn('getUTCDay', DATE, 0),
    utcyear: fn('getUTCFullYear', DATE, 0),
    utcmonth: fn('getUTCMonth', DATE, 0),
    utchours: fn('getUTCHours', DATE, 0),
    utcminutes: fn('getUTCMinutes', DATE, 0),
    utcseconds: fn('getUTCSeconds', DATE, 0),
    utcmilliseconds: fn('getUTCMilliseconds', DATE, 0),
    // sequence functions
    length: fn('length', null, -1),
    // STRING functions
    parseFloat: 'parseFloat',
    parseInt: 'parseInt',
    upper: fn('toUpperCase', STRING, 0),
    lower: fn('toLowerCase', STRING, 0),
    substring: fn('substring', STRING),
    split: fn('split', STRING),
    trim: fn('trim', STRING, 0),
    // base64 encode/decode
    btoa: 'btoa',
    atob: 'atob',
    // REGEXP functions
    regexp: REGEXP,
    test: fn('test', REGEXP),
    // Control Flow functions
    if: function (args) {
      if (args.length < 3) error$1('Missing arguments to if function.');
      if (args.length > 3) error$1('Too many arguments to if function.');
      const a = args.map(codegen);
      return '(' + a[0] + '?' + a[1] + ':' + a[2] + ')';
    }
  };
}

function stripQuotes(s) {
  const n = s && s.length - 1;
  return n && (s[0] === '"' && s[n] === '"' || s[0] === '\'' && s[n] === '\'') ? s.slice(1, -1) : s;
}
function codegen (opt) {
  opt = opt || {};
  const allowed = opt.allowed ? toSet(opt.allowed) : {},
    forbidden = opt.forbidden ? toSet(opt.forbidden) : {},
    constants = opt.constants || Constants$1,
    functions = (opt.functions || Functions$1)(visit),
    globalvar = opt.globalvar,
    fieldvar = opt.fieldvar,
    outputGlobal = isFunction(globalvar) ? globalvar : id => `${globalvar}["${id}"]`;
    // JSON authors are not allowed to set properties with these names, as these are built-in to the JS Object Prototype.
    new Set([...Object.getOwnPropertyNames(Object.prototype).filter(name => typeof Object.prototype[name] === 'function'), '__proto__']);
  let globals = {},
    fields = {},
    memberDepth = 0;
  function visit(ast) {
    if (isString(ast)) return ast;
    const generator = Generators[ast.type];
    if (generator == null) error$1('Unsupported type: ' + ast.type);
    return generator(ast);
  }
  const Generators = {
    Literal: n => n.raw,
    Identifier: n => {
      const id = n.name;
      if (memberDepth > 0) {
        return id;
      } else if (has$1(forbidden, id)) {
        return error$1('Illegal identifier: ' + id);
      } else if (has$1(constants, id)) {
        return constants[id];
      } else if (has$1(allowed, id)) {
        return id;
      } else {
        globals[id] = 1;
        return outputGlobal(id);
      }
    },
    MemberExpression: n => {
      const d = !n.computed,
        o = visit(n.object);
      if (d) memberDepth += 1;
      const p = visit(n.property);
      if (o === fieldvar) {
        // strip quotes to sanitize field name (#1653)
        fields[stripQuotes(p)] = 1;
      }
      if (d) memberDepth -= 1;
      return o + (d ? '.' + p : '[' + p + ']');
    },
    CallExpression: n => {
      if (n.callee.type !== 'Identifier') {
        error$1('Illegal callee type: ' + n.callee.type);
      }
      const callee = n.callee.name,
        args = n.arguments,
        fn = has$1(functions, callee) && functions[callee];
      if (!fn) error$1('Unrecognized function: ' + callee);
      return isFunction(fn) ? fn(args) : fn + '(' + args.map(visit).join(',') + ')';
    },
    ArrayExpression: n => '[' + n.elements.map(visit).join(',') + ']',
    BinaryExpression: n => '(' + visit(n.left) + ' ' + n.operator + ' ' + visit(n.right) + ')',
    UnaryExpression: n => '(' + n.operator + visit(n.argument) + ')',
    ConditionalExpression: n => '(' + visit(n.test) + '?' + visit(n.consequent) + ':' + visit(n.alternate) + ')',
    LogicalExpression: n => '(' + visit(n.left) + n.operator + visit(n.right) + ')',
    ObjectExpression: n => {
      // If any keys would override Object prototype methods, throw error
      for (const prop of n.properties) {
        const keyName = prop.key.name;
        if (DisallowedObjectProperties.has(keyName)) {
          error$1('Illegal property: ' + keyName);
        }
      }
      return '{' + n.properties.map(visit).join(',') + '}';
    },
    Property: n => {
      memberDepth += 1;
      const k = visit(n.key);
      memberDepth -= 1;
      return k + ':' + visit(n.value);
    }
  };
  function codegen(ast) {
    const result = {
      code: visit(ast),
      globals: Object.keys(globals),
      fields: Object.keys(fields)
    };
    globals = {};
    fields = {};
    return result;
  }
  codegen.functions = functions;
  codegen.constants = constants;
  return codegen;
}

// Registers vega-util field accessors to protect against XSS attacks
const SELECTION_GETTER = Symbol('vega_selection_getter');
function getter(f) {
  if (!f.getter || !f.getter[SELECTION_GETTER]) {
    f.getter = field$1(f.field);
    f.getter[SELECTION_GETTER] = true;
  }
  return f.getter;
}
const Intersect = 'intersect';
const Union = 'union';
const VlMulti = 'vlMulti';
const VlPoint = 'vlPoint';
const Or = 'or';
const And = 'and';
const SelectionId = '_vgsid_';
const $selectionId = field$1(SelectionId);

const TYPE_ENUM = 'E',
  TYPE_RANGE_INC = 'R',
  TYPE_RANGE_EXC = 'R-E',
  TYPE_RANGE_LE = 'R-LE',
  TYPE_RANGE_RE = 'R-RE',
  TYPE_PRED_LT = 'E-LT',
  TYPE_PRED_LTE = 'E-LTE',
  TYPE_PRED_GT = 'E-GT',
  TYPE_PRED_GTE = 'E-GTE',
  TYPE_PRED_VALID = 'E-VALID',
  TYPE_PRED_ONE_OF = 'E-ONE',
  UNIT_INDEX = 'index:unit';

// TODO: revisit date coercion?
function testPoint(datum, entry) {
  var fields = entry.fields,
    values = entry.values,
    n = fields.length,
    i = 0,
    dval,
    f;
  for (; i < n; ++i) {
    f = fields[i];
    dval = getter(f)(datum);
    if (isDate$1(dval)) dval = toNumber(dval);
    if (isDate$1(values[i])) values[i] = toNumber(values[i]);
    if (isArray(values[i]) && isDate$1(values[i][0])) values[i] = values[i].map(toNumber);
    if (f.type === TYPE_ENUM) {
      // Enumerated fields can either specify individual values (single/multi selections)
      // or an array of values (interval selections).
      if (isArray(values[i]) ? !values[i].includes(dval) : dval !== values[i]) {
        return false;
      }
    } else {
      if (f.type === TYPE_RANGE_INC) {
        if (!inrange(dval, values[i])) return false;
      } else if (f.type === TYPE_RANGE_RE) {
        // Discrete selection of bins test within the range [bin_start, bin_end).
        if (!inrange(dval, values[i], true, false)) return false;
      } else if (f.type === TYPE_RANGE_EXC) {
        // 'R-E'/'R-LE' included for completeness.
        if (!inrange(dval, values[i], false, false)) return false;
      } else if (f.type === TYPE_RANGE_LE) {
        if (!inrange(dval, values[i], false, true)) return false;
      } else if (f.type === TYPE_PRED_LT) {
        if (dval >= values[i]) return false;
      } else if (f.type === TYPE_PRED_LTE) {
        if (dval > values[i]) return false;
      } else if (f.type === TYPE_PRED_GT) {
        if (dval <= values[i]) return false;
      } else if (f.type === TYPE_PRED_GTE) {
        if (dval < values[i]) return false;
      } else if (f.type === TYPE_PRED_VALID) {
        if (dval === null || isNaN(dval)) return false;
      } else if (f.type === TYPE_PRED_ONE_OF) {
        if (values[i].indexOf(dval) === -1) return false;
      }
    }
  }
  return true;
}

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

      // if we've already matched this unit, skip.
      if (count === -1) continue;
      b = testPoint(datum, entry);
      miss[unit] = b ? -1 : ++count;

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

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

  // if intersecting and we made it here, then we saw no misses
  // if not intersecting, then we saw no matches
  // if no active selections, return false
  return n && intersect;
}
const bisect = bisector($selectionId),
  bisectLeft = bisect.left,
  bisectRight = bisect.right;
function selectionIdTest(name, datum, op) {
  const data = this.context.data[name],
    entries = data ? data.values.value : [],
    unitIdx = data ? data[UNIT_INDEX] && data[UNIT_INDEX].value : undefined,
    intersect = op === Intersect,
    value = $selectionId(datum),
    index = bisectLeft(entries, value);
  if (index === entries.length) return false;
  if ($selectionId(entries[index]) !== value) return false;
  if (unitIdx && intersect) {
    if (unitIdx.size === 1) return true;
    if (bisectRight(entries, value) - index < unitIdx.size) return false;
  }
  return true;
}

/**
 * Maps an array of scene graph items to an array of selection tuples.
 * @param {array} array - Input scene graph items
 * @param {object} base - The base object that generated tuples extend.
 *
 * @returns {array} An array of selection entries for the given unit.
 */
function selectionTuples(array, base) {
  if (!isArray(array)) {
    error$1('First argument to selectionTuples must be an array.');
  }
  if (!isObject(base)) {
    error$1('Second argument to selectionTuples must be an object.');
  }
  return array.map(x => extend(base.fields ? {
    values: base.fields.map(f => getter(f)(x.datum))
  } : {
    [SelectionId]: $selectionId(x.datum)
  }, base));
}

/**
 * Resolves selection for use as a scale domain or reads via the API.
 * @param {string} name - The name of the dataset representing the selection
 * @param {string} [op='union'] - The set operation for combining selections.
 *                 One of 'intersect' or 'union' (default).
 * @param {boolean} isMulti - Identifies a "multi" selection to perform more
 *                 expensive resolution computation.
 * @param {boolean} vl5 - With Vega-Lite v5, "multi" selections are now called "point"
 *                 selections, and thus the resolved tuple should reflect this name.
 *                 This parameter allows us to reflect this change without triggering
 *                 a major version bump for Vega.
 * @returns {object} An object of selected fields and values.
 */
function selectionResolve(name, op, isMulti, vl5) {
  var data = this.context.data[name],
    entries = data ? data.values.value : [],
    resolved = {},
    multiRes = {},
    types = {},
    entry,
    fields,
    values,
    unit,
    field,
    value,
    res,
    resUnit,
    type,
    union,
    n = entries.length,
    i = 0,
    j,
    m;

  // First union all entries within the same unit.
  for (; i < n; ++i) {
    entry = entries[i];
    unit = entry.unit;
    fields = entry.fields;
    values = entry.values;
    if (fields && values) {
      // Intentional selection stores
      for (j = 0, m = fields.length; j < m; ++j) {
        field = fields[j];
        res = resolved[field.field] || (resolved[field.field] = {});
        resUnit = res[unit] || (res[unit] = []);
        types[field.field] = type = field.type.charAt(0);
        union = ops[`${type}_union`];
        res[unit] = union(resUnit, array$2(values[j]));
      }

      // If the same multi-selection is repeated over views and projected over
      // an encoding, it may operate over different fields making it especially
      // tricky to reliably resolve it. At best, we can de-dupe identical entries
      // but doing so may be more computationally expensive than it is worth.
      // Instead, for now, we simply transform our store representation into
      // a more human-friendly one.
      if (isMulti) {
        resUnit = multiRes[unit] || (multiRes[unit] = []);
        resUnit.push(array$2(values).reduce((obj, curr, j) => (obj[fields[j].field] = curr, obj), {}));
      }
    } else {
      // Short circuit extensional selectionId stores which hold sorted IDs unique to each unit.
      field = SelectionId;
      value = $selectionId(entry);
      res = resolved[field] || (resolved[field] = {});
      resUnit = res[unit] || (res[unit] = []);
      resUnit.push(value);
      if (isMulti) {
        resUnit = multiRes[unit] || (multiRes[unit] = []);
        resUnit.push({
          [SelectionId]: value
        });
      }
    }
  }

  // Then resolve fields across units as per the op.
  op = op || Union;
  if (resolved[SelectionId]) {
    resolved[SelectionId] = ops[`${SelectionId}_${op}`](...Object.values(resolved[SelectionId]));
  } else {
    Object.keys(resolved).forEach(field => {
      resolved[field] = Object.keys(resolved[field]).map(unit => resolved[field][unit]).reduce((acc, curr) => acc === undefined ? curr : ops[`${types[field]}_${op}`](acc, curr));
    });
  }
  entries = Object.keys(multiRes);
  if (isMulti && entries.length) {
    const key = vl5 ? VlPoint : VlMulti;
    resolved[key] = op === Union ? {
      [Or]: entries.reduce((acc, k) => (acc.push(...multiRes[k]), acc), [])
    } : {
      [And]: entries.map(k => ({
        [Or]: multiRes[k]
      }))
    };
  }
  return resolved;
}
var ops = {
  [`${SelectionId}_union`]: union,
  [`${SelectionId}_intersect`]: intersection,
  E_union: function (base, value) {
    if (!base.length) return value;
    var i = 0,
      n = value.length;
    for (; i < n; ++i) if (!base.includes(value[i])) base.push(value[i]);
    return base;
  },
  E_intersect: function (base, value) {
    return !base.length ? value : base.filter(v => value.includes(v));
  },
  R_union: function (base, value) {
    var lo = toNumber(value[0]),
      hi = toNumber(value[1]);
    if (lo > hi) {
      lo = value[1];
      hi = value[0];
    }
    if (!base.length) return [lo, hi];
    if (base[0] > lo) base[0] = lo;
    if (base[1] < hi) base[1] = hi;
    return base;
  },
  R_intersect: function (base, value) {
    var lo = toNumber(value[0]),
      hi = toNumber(value[1]);
    if (lo > hi) {
      lo = value[1];
      hi = value[0];
    }
    if (!base.length) return [lo, hi];
    if (hi < base[0] || base[1] < lo) {
      return [];
    } else {
      if (base[0] < lo) base[0] = lo;
      if (base[1] > hi) base[1] = hi;
    }
    return base;
  }
};

const DataPrefix$1 = ':',
  IndexPrefix$1 = '@';
function selectionVisitor(name, args, scope, params) {
  if (args[0].type !== Literal) error$1('First argument to selection functions must be a string literal.');
  const data = args[0].value,
    op = args.length >= 2 && peek$1(args).value,
    field = 'unit',
    indexName = IndexPrefix$1 + field,
    dataName = DataPrefix$1 + data;
  if (op === Intersect && !has$1(params, indexName)) {
    params[indexName] = scope.getData(data).indataRef(scope, field);
  }
  if (!has$1(params, dataName)) {
    params[dataName] = scope.getData(data).tuplesRef();
  }
}

function data$1(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(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 value === null ? 'null' : locale[method](spec)(value);
};
const format = wrap('format');
const timeFormat$1 = wrap('timeFormat');
const utcFormat = wrap('utcFormat');
const timeParse = wrap('timeParse');
const utcParse = wrap('utcParse');
const 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$1.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 !== Literal) {
    error$1('First argument to data functions must be a string literal.');
  }
  const data = args[0].value,
    dataName = DataPrefix + data;
  if (!has$1(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 !== Literal) error$1('First argument to indata must be a string literal.');
  if (args[1].type !== Literal) error$1('Second argument to indata must be a string literal.');
  const data = args[0].value,
    field = args[1].value,
    indexName = IndexPrefix + field;
  if (!has$1(indexName, params)) {
    params[indexName] = scope.getData(data).indataRef(scope, field);
  }
}
function scaleVisitor(name, args, scope, params) {
  if (args[0].type === 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 (!has$1(params, scaleName)) {
    try {
      params[scaleName] = scope.scaleRef(name);
    } catch (err) {
      // TODO: error handling? warning?
    }
  }
}

/**
 * nameOrFunction must be a string or function that was registered.
 * Return undefined if scale is not recognized.
 */
function getScale(nameOrFunction, ctx) {
  if (isString(nameOrFunction)) {
    const maybeScale = ctx.scales[nameOrFunction];
    return maybeScale && isRegisteredScale(maybeScale.value) ? maybeScale.value : undefined;
  } else if (isFunction(nameOrFunction)) {
    return isRegisteredScale(nameOrFunction) ? nameOrFunction : undefined;
  }
  return 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 === Literal ? $(ScalePrefix + arg.value) : $(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', geoArea$1);
const geoBounds = geoMethod('bounds', geoBounds$1);
const geoCentroid = geoMethod('centroid', geoCentroid$1);
function geoScale(projection, group) {
  const p = getScale(projection, (group || this).context);
  return p && p.scale();
}

function inScope (item) {
  const group = this.context.group;
  let 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$1() {
  return log(this.context.dataflow, 'warn', arguments);
}
function info() {
  return log(this.context.dataflow, 'info', arguments);
}
function debug$1() {
  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 = 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 () {
  const args = [].slice.call(arguments);
  args.unshift({});
  return extend(...args);
}

function equal(a, b) {
  return a === b || a !== a && b !== b ? true : isArray(a) ? isArray(b) && a.length === b.length ? equalArray(a, b) : false : isObject(a) && 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 (const key in a) {
    if (!equal(a[key], b[key])) return false;
  }
  return true;
}
function removePredicate(props) {
  return _ => equalObject(props, _);
}

/**
 * Modify data in a dataset.
 * @param {string} name - Dataset name
 * @param {Array|Object} insert - Data to insert
 * @param {boolean|Array|Object} remove - true to remove all, array/tuple to remove, or object to match
 * @param {Object} toggle - Data to toggle
 * @param {Object|Array} modify - Tuple or array of tuples to modify
 * @param {Object} values - Field values to update
 */
function modify (name, insert, remove, toggle, modify, values) {
  const df = this.context.dataflow,
    data = this.context.data[name],
    input = data.input,
    stamp = df.stamp();
  let changes = data.changes,
    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(() => {
      data.modified = true;
      df.pulse(input, changes).run();
    }, true, 1);
  }
  if (remove) {
    predicate = remove === true ? truthy : isArray(remove) || 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) {
    if (isFunction(modify)) {
      throw Error('modify parameter must be a data tuple, not a function');
    }
    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.hypot(dx, dy);
}
function pinchAngle(event) {
  const t = event.touches;
  return Math.atan2(t[0].clientY - t[1].clientY, t[0].clientX - t[1].clientX);
}

// memoize accessor functions
const accessors = {};
function pluck (data, name) {
  const accessor = accessors[name] || (accessors[name] = field$1(name));
  return isArray(data) ? data.map(accessor) : accessor(data);
}

function array(seq) {
  return isArray(seq) || ArrayBuffer.isView(seq) ? seq : null;
}
function sequence(seq) {
  return array(seq) || (isString(seq) ? seq : null);
}
function join(seq, ...args) {
  return array(seq).join(...args);
}
function indexof(seq, ...args) {
  return sequence(seq).indexOf(...args);
}
function lastindexof(seq, ...args) {
  return sequence(seq).lastIndexOf(...args);
}
function slice$1(seq, ...args) {
  return sequence(seq).slice(...args);
}
function replace(str, pattern, repl) {
  if (isFunction(repl)) error$1('Function argument passed to replace.');
  if (!isString(pattern) && !isRegExp(pattern)) error$1('Please pass a string or RegExp argument to replace.');
  return String(str).replace(pattern, repl);
}
function reverse$1(seq) {
  return array(seq).slice().reverse();
}
function sort(seq) {
  return array(seq).slice().sort(ascending$1);
}

function bandspace(count, paddingInner, paddingOuter) {
  return 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$2(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 : isArray(range) ? (s.invertRange || s.invert)(range) : (s.invert || s.invertExtent)(range);
}
function range$3(name, group) {
  const s = getScale(name, (group || this).context);
  return s && s.range ? s.range() : [];
}
function scale$2(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 = Gradient$1(p0, p1);
  let stops = scale.domain(),
    min = stops[0],
    max = peek$1(stops),
    fraction = identity;
  if (!(max - min)) {
    // expand scale if domain has zero span, fix #1479
    scale = (scale.interpolator ? scale$4('sequential')().interpolator(scale.interpolator()) : scale$4('linear')().interpolate(scale.interpolate()).range(scale.range())).domain([min = 0, max = 1]);
  } else {
    fraction = scaleFraction(scale, min, max);
  }
  if (scale.ticks) {
    stops = scale.ticks(+count || 15);
    if (min !== stops[0]) stops.unshift(min);
    if (max !== peek$1(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 ? pathRender(context, p = p || parse$3(path)) : path;
  };
}

const datum = d => d.data;
function treeNodes(name, context) {
  const tree = data$1.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 Bounds().set(u[0], u[1], v[0], v[1]),
    scene = group || this.context.dataflow.scenegraph().root;
  return intersect$2(scene, box, filter(opt));
}
function filter(opt) {
  let p = null;
  if (opt) {
    const types = array$2(opt.marktype),
      names = array$2(opt.markname);
    p = _ => (!types.length || types.some(t => _.marktype === t)) && (!names.length || names.some(s => _.name === s));
  }
  return p;
}

/**
 * Appends a new point to the lasso
 *
 * @param {*} lasso the lasso in pixel space
 * @param {*} x the x coordinate in pixel space
 * @param {*} y the y coordinate in pixel space
 * @param {*} minDist the minimum distance, in pixels, that thenew point needs to be apart from the last point
 * @returns a new array containing the lasso with the new point
 */
function lassoAppend(lasso, x, y, minDist = 5) {
  lasso = array$2(lasso);
  const last = lasso[lasso.length - 1];

  // Add point to lasso if its the first point or distance to last point exceed minDist
  return last === undefined || Math.hypot(last[0] - x, last[1] - y) > minDist ? [...lasso, [x, y]] : lasso;
}

/**
 * Generates a svg path command which draws a lasso
 *
 * @param {*} lasso the lasso in pixel space in the form [[x,y], [x,y], ...]
 * @returns the svg path command that draws the lasso
 */
function lassoPath(lasso) {
  return array$2(lasso).reduce((svg, [x, y], i) => {
    return svg += i == 0 ? `M ${x},${y} ` : i === lasso.length - 1 ? ' Z' : `L ${x},${y} `;
  }, '');
}

/**
 * Inverts the lasso from pixel space to an array of vega scenegraph tuples
 *
 * @param {*} data the dataset
 * @param {*} pixelLasso the lasso in pixel space, [[x,y], [x,y], ...]
 * @param {*} unit the unit where the lasso is defined
 *
 * @returns an array of vega scenegraph tuples
 */
function intersectLasso(markname, pixelLasso, unit) {
  const {
    x,
    y,
    mark
  } = unit;
  const bb = new Bounds().set(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);

  // Get bounding box around lasso
  for (const [px, py] of pixelLasso) {
    if (px < bb.x1) bb.x1 = px;
    if (px > bb.x2) bb.x2 = px;
    if (py < bb.y1) bb.y1 = py;
    if (py > bb.y2) bb.y2 = py;
  }

  // Translate bb against unit coordinates
  bb.translate(x, y);
  const intersection = intersect([[bb.x1, bb.y1], [bb.x2, bb.y2]], markname, mark);

  // Check every point against the lasso
  return intersection.filter(tuple => pointInPolygon(tuple.x, tuple.y, pixelLasso));
}

/**
 * Performs a test if a point is inside a polygon based on the idea from
 * https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
 *
 * This method will not need the same start/end point since it wraps around the edges of the array
 *
 * @param {*} test a point to test against
 * @param {*} polygon a polygon in the form [[x,y], [x,y], ...]
 * @returns true if the point lies inside the polygon, false otherwise
 */
function pointInPolygon(testx, testy, polygon) {
  let intersections = 0;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const [prevX, prevY] = polygon[j];
    const [x, y] = polygon[i];

    // count intersections
    if (y > testy != prevY > testy && testx < (prevX - x) * (testy - y) / (prevY - y) + x) {
      intersections++;
    }
  }

  // point is in polygon if intersection count is odd
  return intersections & 1;
}

// Expression function context object
const functionContext = {
  random() {
    return random();
  },
  // override default
  cumulativeNormal,
  cumulativeLogNormal,
  cumulativeUniform,
  densityNormal,
  densityLogNormal,
  densityUniform,
  quantileNormal,
  quantileLogNormal,
  quantileUniform,
  sampleNormal,
  sampleLogNormal,
  sampleUniform,
  isArray,
  isBoolean: isBoolean$2,
  isDate: isDate$1,
  isDefined(_) {
    return _ !== undefined;
  },
  isNumber: isNumber$1,
  isObject,
  isRegExp,
  isString,
  isTuple,
  isValid(_) {
    return _ != null && _ === _;
  },
  toBoolean,
  toDate(_) {
    return toDate(_);
  },
  // suppress extra arguments
  toNumber,
  toString,
  indexof,
  join,
  lastindexof,
  replace,
  reverse: reverse$1,
  sort,
  slice: slice$1,
  flush,
  lerp,
  merge,
  pad,
  peek: peek$1,
  pluck,
  span,
  inrange,
  truncate: truncate$1,
  rgb,
  lab,
  hcl,
  hsl,
  luminance,
  contrast,
  sequence: range$4,
  format,
  utcFormat,
  utcParse,
  utcOffset,
  utcSequence,
  timeFormat: timeFormat$1,
  timeParse,
  timeOffset,
  timeSequence,
  timeUnitSpecifier,
  monthFormat,
  monthAbbrevFormat,
  dayFormat,
  dayAbbrevFormat,
  quarter,
  utcquarter,
  week,
  utcweek,
  dayofyear,
  utcdayofyear,
  warn: warn$1,
  info,
  debug: debug$1,
  extent(_) {
    return extent(_);
  },
  // suppress extra arguments
  inScope,
  intersect,
  clampRange,
  pinchDistance,
  pinchAngle,
  screen,
  containerSize,
  windowSize,
  bandspace,
  setdata,
  pathShape,
  panLinear,
  panLog,
  panPow,
  panSymlog,
  zoomLinear,
  zoomLog,
  zoomPow,
  zoomSymlog,
  encode,
  modify,
  lassoAppend,
  lassoPath,
  intersectLasso
};
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

// export code generator parameters
const codegenParams = {
  forbidden: ['_'],
  allowed: ['datum', 'event', 'item'],
  fieldvar: 'datum',
  globalvar: id => `_[${$(SignalPrefix + id)}]`,
  functions: buildFunctions,
  constants: Constants$1,
  visitors: astVisitors
};

// export code generator
const codeGenerator = codegen(codegenParams);

// Build expression function registry
function buildFunctions(codegen) {
  const fn = Functions$1(codegen);
  eventFunctions.forEach(name => fn[name] = eventPrefix + name);
  for (const name in functionContext) {
    fn[name] = thisPrefix + name;
  }
  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$2, scaleVisitor);
expressionFunction('range', range$3, scaleVisitor);
expressionFunction('invert', invert, scaleVisitor);
expressionFunction('scale', scale$2, scaleVisitor);
expressionFunction('gradient', scaleGradient, scaleVisitor);
expressionFunction('geoArea', geoArea, scaleVisitor);
expressionFunction('geoBounds', geoBounds, scaleVisitor);
expressionFunction('geoCentroid', geoCentroid, scaleVisitor);
expressionFunction('geoShape', geoShape, scaleVisitor);
expressionFunction('geoScale', geoScale, scaleVisitor);
expressionFunction('indata', indata, indataVisitor);
expressionFunction('data', data$1, dataVisitor);
expressionFunction('treePath', treePath, dataVisitor);
expressionFunction('treeAncestors', treeAncestors, dataVisitor);

// register Vega-Lite selection functions
expressionFunction('vlSelectionTest', selectionTest, selectionVisitor);
expressionFunction('vlSelectionIdTest', selectionIdTest, selectionVisitor);
expressionFunction('vlSelectionResolve', selectionResolve, selectionVisitor);
expressionFunction('vlSelectionTuples', selectionTuples);

function parser (expr, scope) {
  const params = {};

  // parse the expression to an abstract syntax tree (ast)
  let ast;
  try {
    expr = isString(expr) ? expr : $(expr) + '';
    ast = parser$1(expr);
  } catch (err) {
    error$1('Expression parse error: ' + expr);
  }

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

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

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

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

/**
 * Parse a serialized dataflow specification.
 */
function parse$2 (spec) {
  const ctx = this,
    operators = spec.operators || [];

  // parse background
  if (spec.background) {
    ctx.background = spec.background;
  }

  // parse event configuration
  if (spec.eventConfig) {
    ctx.eventConfig = spec.eventConfig;
  }

  // parse locale configuration
  if (spec.locale) {
    ctx.locale = spec.locale;
  }

  // parse operators
  operators.forEach(entry => ctx.parseOperator(entry));

  // parse operator parameters
  operators.forEach(entry => ctx.parseOperatorParameters(entry));

  // parse streams
  (spec.streams || []).forEach(entry => ctx.parseStream(entry));

  // parse updates
  (spec.updates || []).forEach(entry => ctx.parseUpdate(entry));
  return ctx.resolve();
}

const Skip$2 = toSet(['rule']),
  Swap = toSet(['group', 'image', 'rect']);
function adjustSpatial$1(encode, marktype) {
  let code = '';
  if (Skip$2[marktype]) return code;
  if (encode.x2) {
    if (encode.x) {
      if (Swap[marktype]) {
        code += 'if(o.x>o.x2)$=o.x,o.x=o.x2,o.x2=$;';
      }
      code += 'o.width=o.x2-o.x;';
    } else {
      code += 'o.x=o.x2-(o.width||0);';
    }
  }
  if (encode.xc) {
    code += 'o.x=o.xc-(o.width||0)/2;';
  }
  if (encode.y2) {
    if (encode.y) {
      if (Swap[marktype]) {
        code += 'if(o.y>o.y2)$=o.y,o.y=o.y2,o.y2=$;';
      }
      code += 'o.height=o.y2-o.y;';
    } else {
      code += 'o.y=o.y2-(o.height||0);';
    }
  }
  if (encode.yc) {
    code += 'o.y=o.yc-(o.height||0)/2;';
  }
  return code;
}
function canonicalType(type) {
  return (type + '').toLowerCase();
}
function isOperator(type) {
  return canonicalType(type) === 'operator';
}
function isCollect(type) {
  return canonicalType(type) === 'collect';
}

function expression$2(ctx, args, code) {
  // wrap code in return statement if expression does not terminate
  if (!code.endsWith(';')) {
    code = 'return(' + code + ');';
  }
  const fn = Function(...args.concat(code));
  return ctx && ctx.functions ? fn.bind(ctx.functions) : fn;
}

// generate code for comparing a single field
function _compare(u, v, lt, gt) {
  return `((u = ${u}) < (v = ${v}) || u == null) && v != null ? ${lt}
  : (u > v || v == null) && u != null ? ${gt}
  : ((v = v instanceof Date ? +v : v), (u = u instanceof Date ? +u : u)) !== u && v === v ? ${lt}
  : v !== v && u === u ? ${gt} : `;
}
var expressionCodegen = {
  /**
   * Parse an expression used to update an operator value.
   */
  operator: (ctx, expr) => expression$2(ctx, ['_'], expr.code),
  /**
   * Parse an expression provided as an operator parameter value.
   */
  parameter: (ctx, expr) => expression$2(ctx, ['datum', '_'], expr.code),
  /**
   * Parse an expression applied to an event stream.
   */
  event: (ctx, expr) => expression$2(ctx, ['event'], expr.code),
  /**
   * Parse an expression used to handle an event-driven operator update.
   */
  handler: (ctx, expr) => {
    const code = `var datum=event.item&&event.item.datum;return ${expr.code};`;
    return expression$2(ctx, ['_', 'event'], code);
  },
  /**
   * Parse an expression that performs visual encoding.
   */
  encode: (ctx, encode) => {
    const {
      marktype,
      channels
    } = encode;
    let code = 'var o=item,datum=o.datum,m=0,$;';
    for (const name in channels) {
      const o = 'o[' + $(name) + ']';
      code += `$=${channels[name].code};if(${o}!==$)${o}=$,m=1;`;
    }
    code += adjustSpatial$1(channels, marktype);
    code += 'return m;';
    return expression$2(ctx, ['item', '_'], code);
  },
  /**
   * Optimized code generators for access and comparison.
   */
  codegen: {
    get(path) {
      const ref = `[${path.map($).join('][')}]`;
      const get = Function('_', `return _${ref};`);
      get.path = ref;
      return get;
    },
    comparator(fields, orders) {
      let t;
      const map = (f, i) => {
        const o = orders[i];
        let u, v;
        if (f.path) {
          u = `a${f.path}`;
          v = `b${f.path}`;
        } else {
          (t = t || {})['f' + i] = f;
          u = `this.f${i}(a)`;
          v = `this.f${i}(b)`;
        }
        return _compare(u, v, -o, o);
      };
      const fn = Function('a', 'b', 'var u, v; return ' + fields.map(map).join('') + '0;');
      return t ? fn.bind(t) : fn;
    }
  }
};

/**
 * Parse a dataflow operator.
 */
function parseOperator(spec) {
  const ctx = this;
  if (isOperator(spec.type) || !spec.type) {
    ctx.operator(spec, spec.update ? ctx.operatorExpression(spec.update) : null);
  } else {
    ctx.transform(spec, spec.type);
  }
}

/**
 * Parse and assign operator parameters.
 */
function parseOperatorParameters(spec) {
  const ctx = this;
  if (spec.params) {
    const op = ctx.get(spec.id);
    if (!op) error$1('Invalid operator id: ' + spec.id);
    ctx.dataflow.connect(op, op.parameters(ctx.parseParameters(spec.params), spec.react, spec.initonly));
  }
}

/**
 * Parse a set of operator parameters.
 */
function parseParameters$1(spec, params) {
  params = params || {};
  const ctx = this;
  for (const key in spec) {
    const value = spec[key];
    params[key] = isArray(value) ? value.map(v => parseParameter$2(v, ctx, params)) : parseParameter$2(value, ctx, params);
  }
  return params;
}

/**
 * Parse a single parameter.
 */
function parseParameter$2(spec, ctx, params) {
  if (!spec || !isObject(spec)) return spec;
  for (let i = 0, n = PARSERS.length, p; i < n; ++i) {
    p = PARSERS[i];
    if (has$1(spec, p.key)) {
      return p.parse(spec, ctx, params);
    }
  }
  return spec;
}

/** Reference parsers. */
var PARSERS = [{
  key: '$ref',
  parse: getOperator
}, {
  key: '$key',
  parse: getKey
}, {
  key: '$expr',
  parse: getExpression
}, {
  key: '$field',
  parse: getField
}, {
  key: '$encode',
  parse: getEncode
}, {
  key: '$compare',
  parse: getCompare
}, {
  key: '$context',
  parse: getContext
}, {
  key: '$subflow',
  parse: getSubflow
}, {
  key: '$tupleid',
  parse: getTupleId
}];

/**
 * Resolve an operator reference.
 */
function getOperator(_, ctx) {
  return ctx.get(_.$ref) || error$1('Operator not defined: ' + _.$ref);
}

/**
 * Resolve an expression reference.
 */
function getExpression(_, ctx, params) {
  if (_.$params) {
    // parse expression parameters
    ctx.parseParameters(_.$params, params);
  }
  const k = 'e:' + _.$expr.code;
  return ctx.fn[k] || (ctx.fn[k] = accessor(ctx.parameterExpression(_.$expr), _.$fields));
}

/**
 * Resolve a key accessor reference.
 */
function getKey(_, ctx) {
  const k = 'k:' + _.$key + '_' + !!_.$flat;
  return ctx.fn[k] || (ctx.fn[k] = key(_.$key, _.$flat, ctx.expr.codegen));
}

/**
 * Resolve a field accessor reference.
 */
function getField(_, ctx) {
  if (!_.$field) return null;
  const k = 'f:' + _.$field + '_' + _.$name;
  return ctx.fn[k] || (ctx.fn[k] = field$1(_.$field, _.$name, ctx.expr.codegen));
}

/**
 * Resolve a comparator function reference.
 */
function getCompare(_, ctx) {
  // As of Vega 5.5.3, $tupleid sort is no longer used.
  // Keep here for now for backwards compatibility.
  const k = 'c:' + _.$compare + '_' + _.$order,
    c = array$2(_.$compare).map(_ => _ && _.$tupleid ? tupleid : _);
  return ctx.fn[k] || (ctx.fn[k] = compare$2(c, _.$order, ctx.expr.codegen));
}

/**
 * Resolve an encode operator reference.
 */
function getEncode(_, ctx) {
  const spec = _.$encode,
    encode = {};
  for (const name in spec) {
    const enc = spec[name];
    encode[name] = accessor(ctx.encodeExpression(enc.$expr), enc.$fields);
    encode[name].output = enc.$output;
  }
  return encode;
}

/**
 * Resolve a context reference.
 */
function getContext(_, ctx) {
  return ctx;
}

/**
 * Resolve a recursive subflow specification.
 */
function getSubflow(_, ctx) {
  const spec = _.$subflow;
  return function (dataflow, key, parent) {
    const subctx = ctx.fork().parse(spec),
      op = subctx.get(spec.operators[0].id),
      p = subctx.signals.parent;
    if (p) p.set(parent);
    op.detachSubflow = () => ctx.detach(subctx);
    return op;
  };
}

/**
 * Resolve a tuple id reference.
 */
function getTupleId() {
  return tupleid;
}

/**
 * Parse an event stream specification.
 */
function parseStream$2 (spec) {
  var ctx = this,
    filter = spec.filter != null ? ctx.eventExpression(spec.filter) : undefined,
    stream = spec.stream != null ? ctx.get(spec.stream) : undefined,
    args;
  if (spec.source) {
    stream = ctx.events(spec.source, spec.type, filter);
  } else if (spec.merge) {
    args = spec.merge.map(_ => ctx.get(_));
    stream = args[0].merge.apply(args[0], args.slice(1));
  }
  if (spec.between) {
    args = spec.between.map(_ => ctx.get(_));
    stream = stream.between(args[0], args[1]);
  }
  if (spec.filter) {
    stream = stream.filter(filter);
  }
  if (spec.throttle != null) {
    stream = stream.throttle(+spec.throttle);
  }
  if (spec.debounce != null) {
    stream = stream.debounce(+spec.debounce);
  }
  if (stream == null) {
    error$1('Invalid stream definition: ' + JSON.stringify(spec));
  }
  if (spec.consume) stream.consume(true);
  ctx.stream(spec, stream);
}

/**
 * Parse an event-driven operator update.
 */
function parseUpdate$1 (spec) {
  var ctx = this,
    srcid = isObject(srcid = spec.source) ? srcid.$ref : srcid,
    source = ctx.get(srcid),
    target = null,
    update = spec.update,
    params = undefined;
  if (!source) error$1('Source not defined: ' + spec.source);
  target = spec.target && spec.target.$expr ? ctx.eventExpression(spec.target.$expr) : ctx.get(spec.target);
  if (update && update.$expr) {
    if (update.$params) {
      params = ctx.parseParameters(update.$params);
    }
    update = ctx.handlerExpression(update.$expr);
  }
  ctx.update(spec, source, target, update, params);
}

const SKIP = {
  skip: true
};
function getState$1(options) {
  var ctx = this,
    state = {};
  if (options.signals) {
    var signals = state.signals = {};
    Object.keys(ctx.signals).forEach(key => {
      const op = ctx.signals[key];
      if (options.signals(key, op)) {
        signals[key] = op.value;
      }
    });
  }
  if (options.data) {
    var data = state.data = {};
    Object.keys(ctx.data).forEach(key => {
      const dataset = ctx.data[key];
      if (options.data(key, dataset)) {
        data[key] = dataset.input.value;
      }
    });
  }
  if (ctx.subcontext && options.recurse !== false) {
    state.subcontext = ctx.subcontext.map(ctx => ctx.getState(options));
  }
  return state;
}
function setState$1(state) {
  var ctx = this,
    df = ctx.dataflow,
    data = state.data,
    signals = state.signals;
  Object.keys(signals || {}).forEach(key => {
    df.update(ctx.signals[key], signals[key], SKIP);
  });
  Object.keys(data || {}).forEach(key => {
    df.pulse(ctx.data[key].input, df.changeset().remove(truthy).insert(data[key]));
  });
  (state.subcontext || []).forEach((substate, i) => {
    const subctx = ctx.subcontext[i];
    if (subctx) subctx.setState(substate);
  });
}

/**
 * Context objects store the current parse state.
 * Enables lookup of parsed operators, event streams, accessors, etc.
 * Provides a 'fork' method for creating child contexts for subflows.
 */
function context (df, transforms, functions, expr) {
  return new Context(df, transforms, functions, expr);
}
function Context(df, transforms, functions, expr) {
  this.dataflow = df;
  this.transforms = transforms;
  this.events = df.events.bind(df);
  this.expr = expr || expressionCodegen, this.signals = {};
  this.scales = {};
  this.nodes = {};
  this.data = {};
  this.fn = {};
  if (functions) {
    this.functions = Object.create(functions);
    this.functions.context = this;
  }
}
function Subcontext(ctx) {
  this.dataflow = ctx.dataflow;
  this.transforms = ctx.transforms;
  this.events = ctx.events;
  this.expr = ctx.expr;
  this.signals = Object.create(ctx.signals);
  this.scales = Object.create(ctx.scales);
  this.nodes = Object.create(ctx.nodes);
  this.data = Object.create(ctx.data);
  this.fn = Object.create(ctx.fn);
  if (ctx.functions) {
    this.functions = Object.create(ctx.functions);
    this.functions.context = this;
  }
}
Context.prototype = Subcontext.prototype = {
  fork() {
    const ctx = new Subcontext(this);
    (this.subcontext || (this.subcontext = [])).push(ctx);
    return ctx;
  },
  detach(ctx) {
    this.subcontext = this.subcontext.filter(c => c !== ctx);

    // disconnect all nodes in the subcontext
    // wipe out targets first for better efficiency
    const keys = Object.keys(ctx.nodes);
    for (const key of keys) ctx.nodes[key]._targets = null;
    for (const key of keys) ctx.nodes[key].detach();
    ctx.nodes = null;
  },
  get(id) {
    return this.nodes[id];
  },
  set(id, node) {
    return this.nodes[id] = node;
  },
  add(spec, op) {
    const ctx = this,
      df = ctx.dataflow,
      data = spec.value;
    ctx.set(spec.id, op);
    if (isCollect(spec.type) && data) {
      if (data.$ingest) {
        df.ingest(op, data.$ingest, data.$format);
      } else if (data.$request) {
        df.preload(op, data.$request, data.$format);
      } else {
        df.pulse(op, df.changeset().insert(data));
      }
    }
    if (spec.root) {
      ctx.root = op;
    }
    if (spec.parent) {
      let p = ctx.get(spec.parent.$ref);
      if (p) {
        df.connect(p, [op]);
        op.targets().add(p);
      } else {
        (ctx.unresolved = ctx.unresolved || []).push(() => {
          p = ctx.get(spec.parent.$ref);
          df.connect(p, [op]);
          op.targets().add(p);
        });
      }
    }
    if (spec.signal) {
      ctx.signals[spec.signal] = op;
    }
    if (spec.scale) {
      ctx.scales[spec.scale] = op;
    }
    if (spec.data) {
      for (const name in spec.data) {
        const data = ctx.data[name] || (ctx.data[name] = {});
        spec.data[name].forEach(role => data[role] = op);
      }
    }
  },
  resolve() {
    (this.unresolved || []).forEach(fn => fn());
    delete this.unresolved;
    return this;
  },
  operator(spec, update) {
    this.add(spec, this.dataflow.add(spec.value, update));
  },
  transform(spec, type) {
    this.add(spec, this.dataflow.add(this.transforms[canonicalType(type)]));
  },
  stream(spec, stream) {
    this.set(spec.id, stream);
  },
  update(spec, stream, target, update, params) {
    this.dataflow.on(stream, target, update, params, spec.options);
  },
  // expression parsing
  operatorExpression(expr) {
    return this.expr.operator(this, expr);
  },
  parameterExpression(expr) {
    return this.expr.parameter(this, expr);
  },
  eventExpression(expr) {
    return this.expr.event(this, expr);
  },
  handlerExpression(expr) {
    return this.expr.handler(this, expr);
  },
  encodeExpression(encode) {
    return this.expr.encode(this, encode);
  },
  // parse methods
  parse: parse$2,
  parseOperator,
  parseOperatorParameters,
  parseParameters: parseParameters$1,
  parseStream: parseStream$2,
  parseUpdate: parseUpdate$1,
  // state methods
  getState: getState$1,
  setState: setState$1
};

// initialize aria role and label attributes
function initializeAria(view) {
  const el = view.container();
  if (el) {
    el.setAttribute('role', 'graphics-document');
    el.setAttribute('aria-roleDescription', 'visualization');
    ariaLabel(el, view.description());
  }
}

// update aria-label if we have a DOM container element
function ariaLabel(el, desc) {
  if (el) desc == null ? el.removeAttribute('aria-label') : el.setAttribute('aria-label', desc);
}

function background (view) {
  // respond to background signal
  view.add(null, _ => {
    view._background = _.bg;
    view._resize = 1;
    return _.bg;
  }, {
    bg: view._signals.background
  });
}

const Default = 'default';
function cursor$1 (view) {
  // get cursor signal, add to dataflow if needed
  const cursor = view._signals.cursor || (view._signals.cursor = view.add({
    user: Default,
    item: null
  }));

  // evaluate cursor on each pointermove event
  view.on(view.events('view', 'pointermove'), cursor, (_, event) => {
    const value = cursor.value,
      user = value ? isString(value) ? value : value.user : Default,
      item = event.item && event.item.cursor || null;
    return value && user === value.user && item == value.item ? value : {
      user: user,
      item: item
    };
  });

  // when cursor signal updates, set visible cursor
  view.add(null, function (_) {
    let user = _.cursor,
      item = this.value;
    if (!isString(user)) {
      item = user.item;
      user = user.user;
    }
    setCursor(view, user && user !== Default ? user : item || user);
    return item;
  }, {
    cursor: cursor
  });
}
function setCursor(view, cursor) {
  const el = view.globalCursor() ? typeof document !== 'undefined' && document.body : view.container();
  if (el) {
    return cursor == null ? el.style.removeProperty('cursor') : el.style.cursor = cursor;
  }
}

function dataref(view, name) {
  var data = view._runtime.data;
  if (!has$1(data, name)) {
    error$1('Unrecognized data set: ' + name);
  }
  return data[name];
}
function data(name, values) {
  return arguments.length < 2 ? dataref(this, name).values.value : change.call(this, name, changeset().remove(truthy).insert(values));
}
function change(name, changes) {
  if (!isChangeSet(changes)) {
    error$1('Second argument to changes must be a changeset.');
  }
  const dataset = dataref(this, name);
  dataset.modified = true;
  return this.pulse(dataset.input, changes);
}
function insert(name, _) {
  return change.call(this, name, changeset().insert(_));
}
function remove(name, _) {
  return change.call(this, name, changeset().remove(_));
}

function width(view) {
  var padding = view.padding();
  return Math.max(0, view._viewWidth + padding.left + padding.right);
}
function height(view) {
  var padding = view.padding();
  return Math.max(0, view._viewHeight + padding.top + padding.bottom);
}
function offset(view) {
  var padding = view.padding(),
    origin = view._origin;
  return [padding.left + origin[0], padding.top + origin[1]];
}
function resizeRenderer(view) {
  var origin = offset(view),
    w = width(view),
    h = height(view);
  view._renderer.background(view.background());
  view._renderer.resize(w, h, origin);
  view._handler.origin(origin);
  view._resizeListeners.forEach(handler => {
    try {
      handler(w, h);
    } catch (error) {
      view.error(error);
    }
  });
}

/**
 * Extend an event with additional view-specific methods.
 * Adds a new property ('vega') to an event that provides a number
 * of methods for querying information about the current interaction.
 * The vega object provides the following methods:
 *   view - Returns the backing View instance.
 *   item - Returns the currently active scenegraph item (if any).
 *   group - Returns the currently active scenegraph group (if any).
 *     This method accepts a single string-typed argument indicating the name
 *     of the desired parent group. The scenegraph will be traversed from
 *     the item up towards the root to search for a matching group. If no
 *     argument is provided the enclosing group for the active item is
 *     returned, unless the item it itself a group, in which case it is
 *     returned directly.
 *   xy - Returns a two-element array containing the x and y coordinates for
 *     mouse or touch events. For touch events, this is based on the first
 *     elements in the changedTouches array. This method accepts a single
 *     argument: either an item instance or mark name that should serve as
 *     the reference coordinate system. If no argument is provided the
 *     top-level view coordinate system is assumed.
 *   x - Returns the current x-coordinate, accepts the same arguments as xy.
 *   y - Returns the current y-coordinate, accepts the same arguments as xy.
 * @param {Event} event - The input event to extend.
 * @param {Item} item - The currently active scenegraph item (if any).
 * @return {Event} - The extended input event.
 */
function eventExtend (view, event, item) {
  var r = view._renderer,
    el = r && r.canvas(),
    p,
    e,
    translate;
  if (el) {
    translate = offset(view);
    e = event.changedTouches ? event.changedTouches[0] : event;
    p = point$2(e, el);
    p[0] -= translate[0];
    p[1] -= translate[1];
  }
  event.dataflow = view;
  event.item = item;
  event.vega = extension(view, item, p);
  return event;
}
function extension(view, item, point) {
  const itemGroup = item ? item.mark.marktype === 'group' ? item : item.mark.group : null;
  function group(name) {
    var g = itemGroup,
      i;
    if (name) for (i = item; i; i = i.mark.group) {
      if (i.mark.name === name) {
        g = i;
        break;
      }
    }
    return g && g.mark && g.mark.interactive ? g : {};
  }
  function xy(item) {
    if (!item) return point;
    if (isString(item)) item = group(item);
    const p = point.slice();
    while (item) {
      p[0] -= item.x || 0;
      p[1] -= item.y || 0;
      item = item.mark && item.mark.group;
    }
    return p;
  }
  return {
    view: constant$1(view),
    item: constant$1(item || {}),
    group: group,
    xy: xy,
    x: item => xy(item)[0],
    y: item => xy(item)[1]
  };
}

const VIEW$1 = 'view',
  TIMER = 'timer',
  WINDOW = 'window',
  NO_TRAP = {
    trap: false
  };

/**
 * Initialize event handling configuration.
 * @param {object} config - The configuration settings.
 * @return {object}
 */
function initializeEventConfig(config) {
  const events = extend({
    defaults: {}
  }, config);
  const unpack = (obj, keys) => {
    keys.forEach(k => {
      if (isArray(obj[k])) obj[k] = toSet(obj[k]);
    });
  };
  unpack(events.defaults, ['prevent', 'allow']);
  unpack(events, ['view', 'window', 'selector']);
  return events;
}
function trackEventListener(view, sources, type, handler) {
  view._eventListeners.push({
    type: type,
    sources: array$2(sources),
    handler: handler
  });
}
function prevent(view, type) {
  var def = view._eventConfig.defaults,
    prevent = def.prevent,
    allow = def.allow;
  return prevent === false || allow === true ? false : prevent === true || allow === false ? true : prevent ? prevent[type] : allow ? !allow[type] : view.preventDefault();
}
function permit(view, key, type) {
  const rule = view._eventConfig && view._eventConfig[key];
  if (rule === false || isObject(rule) && !rule[type]) {
    view.warn(`Blocked ${key} ${type} event listener.`);
    return false;
  }
  return true;
}

/**
 * Create a new event stream from an event source.
 * @param {object} source - The event source to monitor.
 * @param {string} type - The event type.
 * @param {function(object): boolean} [filter] - Event filter function.
 * @return {EventStream}
 */
function events(source, type, filter) {
  var view = this,
    s = new EventStream(filter),
    send = function (e, item) {
      view.runAsync(null, () => {
        if (source === VIEW$1 && prevent(view, type)) {
          e.preventDefault();
        }
        s.receive(eventExtend(view, e, item));
      });
    },
    sources;
  if (source === TIMER) {
    if (permit(view, 'timer', type)) {
      view.timer(send, type);
    }
  } else if (source === VIEW$1) {
    if (permit(view, 'view', type)) {
      // send traps errors, so use {trap: false} option
      view.addEventListener(type, send, NO_TRAP);
    }
  } else {
    if (source === WINDOW) {
      if (permit(view, 'window', type) && typeof window !== 'undefined') {
        sources = [window];
      }
    } else if (typeof document !== 'undefined') {
      if (permit(view, 'selector', type)) {
        sources = Array.from(document.querySelectorAll(source));
      }
    }
    if (!sources) {
      view.warn('Can not resolve event source: ' + source);
    } else {
      for (var i = 0, n = sources.length; i < n; ++i) {
        sources[i].addEventListener(type, send);
      }
      trackEventListener(view, sources, type, send);
    }
  }
  return s;
}

function itemFilter(event) {
  return event.item;
}
function markTarget(event) {
  // grab upstream collector feeding the mark operator
  return event.item.mark.source;
}
function invoke(name) {
  return function (_, event) {
    return event.vega.view().changeset().encode(event.item, name);
  };
}
function hover (hoverSet, leaveSet) {
  hoverSet = [hoverSet || 'hover'];
  leaveSet = [leaveSet || 'update', hoverSet[0]];

  // invoke hover set upon pointerover
  this.on(this.events('view', 'pointerover', itemFilter), markTarget, invoke(hoverSet));

  // invoke leave set upon pointerout
  this.on(this.events('view', 'pointerout', itemFilter), markTarget, invoke(leaveSet));
  return this;
}

/**
 * Finalize a View instance that is being removed.
 * Cancel any running timers.
 * Remove all external event listeners.
 * Remove any currently displayed tooltip.
 */
function finalize () {
  var tooltip = this._tooltip,
    timers = this._timers,
    handlers = this._handler.handlers(),
    listeners = this._eventListeners,
    n,
    m,
    e,
    h,
    t;
  n = timers.length;
  while (--n >= 0) {
    timers[n].stop();
  }
  n = listeners.length;
  while (--n >= 0) {
    e = listeners[n];
    m = e.sources.length;
    while (--m >= 0) {
      e.sources[m].removeEventListener(e.type, e.handler);
    }
  }
  if (tooltip) {
    tooltip.call(this, this._handler, null, null, null);
  }

  // turn off all registered handlers
  n = handlers.length;
  while (--n >= 0) {
    t = handlers[n].type;
    h = handlers[n].handler;
    this._handler.off(t, h);
  }
  return this;
}

function element (tag, attr, text) {
  const el = document.createElement(tag);
  for (const key in attr) el.setAttribute(key, attr[key]);
  if (text != null) el.textContent = text;
  return el;
}

const BindClass = 'vega-bind',
  NameClass = 'vega-bind-name',
  RadioClass = 'vega-bind-radio';

/**
 * Bind a signal to an external HTML input element. The resulting two-way
 * binding will propagate input changes to signals, and propagate signal
 * changes to the input element state. If this view instance has no parent
 * element, we assume the view is headless and no bindings are created.
 * @param {Element|string} el - The parent DOM element to which the input
 *   element should be appended as a child. If string-valued, this argument
 *   will be treated as a CSS selector. If null or undefined, the parent
 *   element of this view will be used as the element.
 * @param {object} param - The binding parameters which specify the signal
 *   to bind to, the input element type, and type-specific configuration.
 * @return {View} - This view instance.
 */
function bind (view, el, binding) {
  if (!el) return;
  const param = binding.param;
  let bind = binding.state;
  if (!bind) {
    bind = binding.state = {
      elements: null,
      active: false,
      set: null,
      update: value => {
        if (value != view.signal(param.signal)) {
          view.runAsync(null, () => {
            bind.source = true;
            view.signal(param.signal, value);
          });
        }
      }
    };
    if (param.debounce) {
      bind.update = debounce(param.debounce, bind.update);
    }
  }
  const create = param.input == null && param.element ? target : generate$1;
  create(bind, el, param, view);
  if (!bind.active) {
    view.on(view._signals[param.signal], null, () => {
      bind.source ? bind.source = false : bind.set(view.signal(param.signal));
    });
    bind.active = true;
  }
  return bind;
}

/**
 * Bind the signal to an external EventTarget.
 */
function target(bind, node, param, view) {
  const type = param.event || 'input';
  const handler = () => bind.update(node.value);

  // initialize signal value to external input value
  view.signal(param.signal, node.value);

  // listen for changes on the element
  node.addEventListener(type, handler);

  // register with view, so we can remove it upon finalization
  trackEventListener(view, node, type, handler);

  // propagate change to element
  bind.set = value => {
    node.value = value;
    node.dispatchEvent(event(type));
  };
}
function event(type) {
  return typeof Event !== 'undefined' ? new Event(type) : {
    type
  };
}

/**
 * Generate an HTML input form element and bind it to a signal.
 */
function generate$1(bind, el, param, view) {
  const value = view.signal(param.signal);
  const div = element('div', {
    'class': BindClass
  });
  const wrapper = param.input === 'radio' ? div : div.appendChild(element('label'));
  wrapper.appendChild(element('span', {
    'class': NameClass
  }, param.name || param.signal));
  el.appendChild(div);
  let input = form;
  switch (param.input) {
    case 'checkbox':
      input = checkbox;
      break;
    case 'select':
      input = select;
      break;
    case 'radio':
      input = radio;
      break;
    case 'range':
      input = range$2;
      break;
  }
  input(bind, wrapper, param, value);
}

/**
 * Generates an arbitrary input form element.
 * The input type is controlled via user-provided parameters.
 */
function form(bind, el, param, value) {
  const node = element('input');
  for (const key in param) {
    if (key !== 'signal' && key !== 'element') {
      node.setAttribute(key === 'input' ? 'type' : key, param[key]);
    }
  }
  node.setAttribute('name', param.signal);
  node.value = value;
  el.appendChild(node);
  node.addEventListener('input', () => bind.update(node.value));
  bind.elements = [node];
  bind.set = value => node.value = value;
}

/**
 * Generates a checkbox input element.
 */
function checkbox(bind, el, param, value) {
  const attr = {
    type: 'checkbox',
    name: param.signal
  };
  if (value) attr.checked = true;
  const node = element('input', attr);
  el.appendChild(node);
  node.addEventListener('change', () => bind.update(node.checked));
  bind.elements = [node];
  bind.set = value => node.checked = !!value || null;
}

/**
 * Generates a selection list input element.
 */
function select(bind, el, param, value) {
  const node = element('select', {
      name: param.signal
    }),
    labels = param.labels || [];
  param.options.forEach((option, i) => {
    const attr = {
      value: option
    };
    if (valuesEqual(option, value)) attr.selected = true;
    node.appendChild(element('option', attr, (labels[i] || option) + ''));
  });
  el.appendChild(node);
  node.addEventListener('change', () => {
    bind.update(param.options[node.selectedIndex]);
  });
  bind.elements = [node];
  bind.set = value => {
    for (let i = 0, n = param.options.length; i < n; ++i) {
      if (valuesEqual(param.options[i], value)) {
        node.selectedIndex = i;
        return;
      }
    }
  };
}

/**
 * Generates a radio button group.
 */
function radio(bind, el, param, value) {
  const group = element('span', {
      'class': RadioClass
    }),
    labels = param.labels || [];
  el.appendChild(group);
  bind.elements = param.options.map((option, i) => {
    const attr = {
      type: 'radio',
      name: param.signal,
      value: option
    };
    if (valuesEqual(option, value)) attr.checked = true;
    const input = element('input', attr);
    input.addEventListener('change', () => bind.update(option));
    const label = element('label', {}, (labels[i] || option) + '');
    label.prepend(input);
    group.appendChild(label);
    return input;
  });
  bind.set = value => {
    const nodes = bind.elements,
      n = nodes.length;
    for (let i = 0; i < n; ++i) {
      if (valuesEqual(nodes[i].value, value)) nodes[i].checked = true;
    }
  };
}

/**
 * Generates a slider input element.
 */
function range$2(bind, el, param, value) {
  value = value !== undefined ? value : (+param.max + +param.min) / 2;
  const max = param.max != null ? param.max : Math.max(100, +value) || 100,
    min = param.min || Math.min(0, max, +value) || 0,
    step = param.step || tickStep(min, max, 100);
  const node = element('input', {
    type: 'range',
    name: param.signal,
    min: min,
    max: max,
    step: step
  });
  node.value = value;
  const span = element('span', {}, +value);
  el.appendChild(node);
  el.appendChild(span);
  const update = () => {
    span.textContent = node.value;
    bind.update(+node.value);
  };

  // subscribe to both input and change
  node.addEventListener('input', update);
  node.addEventListener('change', update);
  bind.elements = [node];
  bind.set = value => {
    node.value = value;
    span.textContent = value;
  };
}
function valuesEqual(a, b) {
  return a === b || a + '' === b + '';
}

function initializeRenderer (view, r, el, constructor, scaleFactor, opt) {
  r = r || new constructor(view.loader());
  return r.initialize(el, width(view), height(view), offset(view), scaleFactor, opt).background(view.background());
}

function trap (view, fn) {
  return !fn ? null : function () {
    try {
      fn.apply(this, arguments);
    } catch (error) {
      view.error(error);
    }
  };
}

function initializeHandler (view, prevHandler, el, constructor) {
  // instantiate scenegraph handler
  const handler = new constructor(view.loader(), trap(view, view.tooltip())).scene(view.scenegraph().root).initialize(el, offset(view), view);

  // transfer event handlers
  if (prevHandler) {
    prevHandler.handlers().forEach(h => {
      handler.on(h.type, h.handler);
    });
  }
  return handler;
}

function initialize (el, elBind) {
  const view = this,
    type = view._renderType,
    config = view._eventConfig.bind,
    module = renderModule(type);

  // containing dom element
  el = view._el = el ? lookup$1(view, el, true) : null;

  // initialize aria attributes
  initializeAria(view);

  // select appropriate renderer & handler
  if (!module) view.error('Unrecognized renderer type: ' + type);
  const Handler = module.handler || CanvasHandler,
    Renderer = el ? module.renderer : module.headless;

  // initialize renderer and input handler
  view._renderer = !Renderer ? null : initializeRenderer(view, view._renderer, el, Renderer);
  view._handler = initializeHandler(view, view._handler, el, Handler);
  view._redraw = true;

  // initialize signal bindings
  if (el && config !== 'none') {
    elBind = elBind ? view._elBind = lookup$1(view, elBind, true) : el.appendChild(element('form', {
      'class': 'vega-bindings'
    }));
    view._bind.forEach(_ => {
      if (_.param.element && config !== 'container') {
        _.element = lookup$1(view, _.param.element, !!_.param.input);
      }
    });
    view._bind.forEach(_ => {
      bind(view, _.element || elBind, _);
    });
  }
  return view;
}
function lookup$1(view, el, clear) {
  if (typeof el === 'string') {
    if (typeof document !== 'undefined') {
      el = document.querySelector(el);
      if (!el) {
        view.error('Signal bind element not found: ' + el);
        return null;
      }
    } else {
      view.error('DOM document instance not found.');
      return null;
    }
  }
  if (el && clear) {
    try {
      el.textContent = '';
    } catch (e) {
      el = null;
      view.error(e);
    }
  }
  return el;
}

const number$1 = _ => +_ || 0;
const paddingObject$1 = _ => ({
  top: _,
  bottom: _,
  left: _,
  right: _
});
function padding$1 (_) {
  return isObject(_) ? {
    top: number$1(_.top),
    bottom: number$1(_.bottom),
    left: number$1(_.left),
    right: number$1(_.right)
  } : paddingObject$1(number$1(_));
}

/**
 * Render the current scene in a headless fashion.
 * This method is asynchronous, returning a Promise instance.
 * @return {Promise} - A Promise that resolves to a renderer.
 */
async function renderHeadless (view, type, scaleFactor, opt) {
  const module = renderModule(type),
    ctr = module && module.headless;
  if (!ctr) error$1('Unrecognized renderer type: ' + type);
  await view.runAsync();
  return initializeRenderer(view, null, null, ctr, scaleFactor, opt).renderAsync(view._scenegraph.root);
}

/**
 * Produce an image URL for the visualization. Depending on the type
 * parameter, the generated URL contains data for either a PNG or SVG image.
 * The URL can be used (for example) to download images of the visualization.
 * This method is asynchronous, returning a Promise instance.
 * @param {string} type - The image type. One of 'svg', 'png' or 'canvas'.
 *   The 'canvas' and 'png' types are synonyms for a PNG image.
 * @return {Promise} - A promise that resolves to an image URL.
 */
async function renderToImageURL (type, scaleFactor) {
  if (type !== RenderType.Canvas && type !== RenderType.SVG && type !== RenderType.PNG) {
    error$1('Unrecognized image type: ' + type);
  }
  const r = await renderHeadless(this, type, scaleFactor);
  return type === RenderType.SVG ? toBlobURL(r.svg(), 'image/svg+xml') : r.canvas().toDataURL('image/png');
}
function toBlobURL(data, mime) {
  const blob = new Blob([data], {
    type: mime
  });
  return window.URL.createObjectURL(blob);
}

/**
 * Produce a Canvas instance containing a rendered visualization.
 * This method is asynchronous, returning a Promise instance.
 * @return {Promise} - A promise that resolves to a Canvas instance.
 */
async function renderToCanvas (scaleFactor, opt) {
  const r = await renderHeadless(this, RenderType.Canvas, scaleFactor, opt);
  return r.canvas();
}

/**
 * Produce a rendered SVG string of the visualization.
 * This method is asynchronous, returning a Promise instance.
 * @return {Promise} - A promise that resolves to an SVG string.
 */
async function renderToSVG (scaleFactor) {
  const r = await renderHeadless(this, RenderType.SVG, scaleFactor);
  return r.svg();
}

function runtime (view, spec, expr) {
  return context(view, transforms, functionContext, expr).parse(spec);
}

function scale$1(name) {
  var scales = this._runtime.scales;
  if (!has$1(scales, name)) {
    error$1('Unrecognized scale or projection: ' + name);
  }
  return scales[name].value;
}

var Width = 'width',
  Height = 'height',
  Padding = 'padding',
  Skip$1 = {
    skip: true
  };
function viewWidth(view, width) {
  var a = view.autosize(),
    p = view.padding();
  return width - (a && a.contains === Padding ? p.left + p.right : 0);
}
function viewHeight(view, height) {
  var a = view.autosize(),
    p = view.padding();
  return height - (a && a.contains === Padding ? p.top + p.bottom : 0);
}
function initializeResize(view) {
  var s = view._signals,
    w = s[Width],
    h = s[Height],
    p = s[Padding];
  function resetSize() {
    view._autosize = view._resize = 1;
  }

  // respond to width signal
  view._resizeWidth = view.add(null, _ => {
    view._width = _.size;
    view._viewWidth = viewWidth(view, _.size);
    resetSize();
  }, {
    size: w
  });

  // respond to height signal
  view._resizeHeight = view.add(null, _ => {
    view._height = _.size;
    view._viewHeight = viewHeight(view, _.size);
    resetSize();
  }, {
    size: h
  });

  // respond to padding signal
  const resizePadding = view.add(null, resetSize, {
    pad: p
  });

  // set rank to run immediately after source signal
  view._resizeWidth.rank = w.rank + 1;
  view._resizeHeight.rank = h.rank + 1;
  resizePadding.rank = p.rank + 1;
}
function resizeView(viewWidth, viewHeight, width, height, origin, auto) {
  this.runAfter(view => {
    let rerun = 0;

    // reset autosize flag
    view._autosize = 0;

    // width value changed: update signal, skip resize op
    if (view.width() !== width) {
      rerun = 1;
      view.signal(Width, width, Skip$1); // set width, skip update calc
      view._resizeWidth.skip(true); // skip width resize handler
    }

    // height value changed: update signal, skip resize op
    if (view.height() !== height) {
      rerun = 1;
      view.signal(Height, height, Skip$1); // set height, skip update calc
      view._resizeHeight.skip(true); // skip height resize handler
    }

    // view width changed: update view property, set resize flag
    if (view._viewWidth !== viewWidth) {
      view._resize = 1;
      view._viewWidth = viewWidth;
    }

    // view height changed: update view property, set resize flag
    if (view._viewHeight !== viewHeight) {
      view._resize = 1;
      view._viewHeight = viewHeight;
    }

    // origin changed: update view property, set resize flag
    if (view._origin[0] !== origin[0] || view._origin[1] !== origin[1]) {
      view._resize = 1;
      view._origin = origin;
    }

    // run dataflow on width/height signal change
    if (rerun) view.run('enter');
    if (auto) view.runAfter(v => v.resize());
  }, false, 1);
}

/**
 * Get the current view state, consisting of signal values and/or data sets.
 * @param {object} [options] - Options flags indicating which state to export.
 *   If unspecified, all signals and data sets will be exported.
 * @param {function(string, Operator):boolean} [options.signals] - Optional
 *   predicate function for testing if a signal should be included in the
 *   exported state. If unspecified, all signals will be included, except for
 *   those named 'parent' or those which refer to a Transform value.
 * @param {function(string, object):boolean} [options.data] - Optional
 *   predicate function for testing if a data set's input should be included
 *   in the exported state. If unspecified, all data sets that have been
 *   explicitly modified will be included.
 * @param {boolean} [options.recurse=true] - Flag indicating if the exported
 *   state should recursively include state from group mark sub-contexts.
 * @return {object} - An object containing the exported state values.
 */
function getState(options) {
  return this._runtime.getState(options || {
    data: dataTest,
    signals: signalTest,
    recurse: true
  });
}
function dataTest(name, data) {
  return data.modified && isArray(data.input.value) && !name.startsWith('_:vega:_');
}
function signalTest(name, op) {
  return !(name === 'parent' || op instanceof transforms.proxy);
}

/**
 * Sets the current view state and updates the view by invoking run.
 * @param {object} state - A state object containing signal and/or
 *   data set values, following the format used by the getState method.
 * @return {View} - This view instance.
 */
function setState(state) {
  this.runAsync(null, v => {
    v._trigger = false;
    v._runtime.setState(state);
  }, v => {
    v._trigger = true;
  });
  return this;
}

function timer (callback, delay) {
  function tick(elapsed) {
    callback({
      timestamp: Date.now(),
      elapsed: elapsed
    });
  }
  this._timers.push(interval$1(tick, delay));
}

function defaultTooltip (handler, event, item, value) {
  const el = handler.element();
  if (el) el.setAttribute('title', formatTooltip(value));
}
function formatTooltip(value) {
  return value == null ? '' : isArray(value) ? formatArray(value) : isObject(value) && !isDate$1(value) ? formatObject(value) : value + '';
}
function formatObject(obj) {
  return Object.keys(obj).map(key => {
    const v = obj[key];
    return key + ': ' + (isArray(v) ? formatArray(v) : formatValue$1(v));
  }).join('\n');
}
function formatArray(value) {
  return '[' + value.map(formatValue$1).join(', ') + ']';
}
function formatValue$1(value) {
  return isArray(value) ? '[\u2026]' : isObject(value) && !isDate$1(value) ? '{\u2026}' : value;
}

function watchPixelRatio () {
  // based on https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
  if (this.renderer() === 'canvas' && this._renderer._canvas) {
    let remove = null;
    const updatePixelRatio = () => {
      if (remove != null) {
        remove();
      }
      const media = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
      media.addEventListener('change', updatePixelRatio);
      remove = () => {
        media.removeEventListener('change', updatePixelRatio);
      };
      this._renderer._canvas.getContext('2d').pixelRatio = window.devicePixelRatio || 1;
      this._redraw = true;
      this._resize = 1;
      this.resize().runAsync();
    };
    updatePixelRatio();
  }
}

/**
 * Create a new View instance from a Vega dataflow runtime specification.
 * The generated View will not immediately be ready for display. Callers
 * should also invoke the initialize method (e.g., to set the parent
 * DOM element in browser-based deployment) and then invoke the run
 * method to evaluate the dataflow graph. Rendering will automatically
 * be performed upon dataflow runs.
 * @constructor
 * @param {object} spec - The Vega dataflow runtime specification.
 */
function View$1(spec, options) {
  const view = this;
  options = options || {};
  Dataflow.call(view);
  if (options.loader) view.loader(options.loader);
  if (options.logger) view.logger(options.logger);
  if (options.logLevel != null) view.logLevel(options.logLevel);
  if (options.locale || spec.locale) {
    const loc = extend({}, spec.locale, options.locale);
    view.locale(locale(loc.number, loc.time));
  }
  view._el = null;
  view._elBind = null;
  view._renderType = options.renderer || RenderType.Canvas;
  view._scenegraph = new Scenegraph();
  const root = view._scenegraph.root;

  // initialize renderer, handler and event management
  view._renderer = null;
  view._tooltip = options.tooltip || defaultTooltip, view._redraw = true;
  view._handler = new CanvasHandler().scene(root);
  view._globalCursor = false;
  view._preventDefault = false;
  view._timers = [];
  view._eventListeners = [];
  view._resizeListeners = [];

  // initialize event configuration
  view._eventConfig = initializeEventConfig(spec.eventConfig);
  view.globalCursor(view._eventConfig.globalCursor);

  // initialize dataflow graph
  const ctx = runtime(view, spec, options.expr);
  view._runtime = ctx;
  view._signals = ctx.signals;
  view._bind = (spec.bindings || []).map(_ => ({
    state: null,
    param: extend({}, _)
  }));

  // initialize scenegraph
  if (ctx.root) ctx.root.set(root);
  root.source = ctx.data.root.input;
  view.pulse(ctx.data.root.input, view.changeset().insert(root.items));

  // initialize view size
  view._width = view.width();
  view._height = view.height();
  view._viewWidth = viewWidth(view, view._width);
  view._viewHeight = viewHeight(view, view._height);
  view._origin = [0, 0];
  view._resize = 0;
  view._autosize = 1;
  initializeResize(view);

  // initialize background color
  background(view);

  // initialize cursor
  cursor$1(view);

  // initialize view description
  view.description(spec.description);

  // initialize hover proessing, if requested
  if (options.hover) view.hover();

  // initialize DOM container(s) and renderer
  if (options.container) view.initialize(options.container, options.bind);
  if (options.watchPixelRatio) view._watchPixelRatio();
}
function lookupSignal(view, name) {
  return has$1(view._signals, name) ? view._signals[name] : error$1('Unrecognized signal name: ' + $(name));
}
function findOperatorHandler(op, handler) {
  const h = (op._targets || []).filter(op => op._update && op._update.handler === handler);
  return h.length ? h[0] : null;
}
function addOperatorListener(view, name, op, handler) {
  let h = findOperatorHandler(op, handler);
  if (!h) {
    h = trap(view, () => handler(name, op.value));
    h.handler = handler;
    view.on(op, null, h);
  }
  return view;
}
function removeOperatorListener(view, op, handler) {
  const h = findOperatorHandler(op, handler);
  if (h) op._targets.remove(h);
  return view;
}
inherits(View$1, Dataflow, {
  // -- DATAFLOW / RENDERING ----

  async evaluate(encode, prerun, postrun) {
    // evaluate dataflow and prerun
    await Dataflow.prototype.evaluate.call(this, encode, prerun);

    // render as needed
    if (this._redraw || this._resize) {
      try {
        if (this._renderer) {
          if (this._resize) {
            this._resize = 0;
            resizeRenderer(this);
          }
          await this._renderer.renderAsync(this._scenegraph.root);
        }
        this._redraw = false;
      } catch (e) {
        this.error(e);
      }
    }

    // evaluate postrun
    if (postrun) asyncCallback(this, postrun);
    return this;
  },
  dirty(item) {
    this._redraw = true;
    this._renderer && this._renderer.dirty(item);
  },
  // -- GET / SET ----

  description(text) {
    if (arguments.length) {
      const desc = text != null ? text + '' : null;
      if (desc !== this._desc) ariaLabel(this._el, this._desc = desc);
      return this;
    }
    return this._desc;
  },
  container() {
    return this._el;
  },
  scenegraph() {
    return this._scenegraph;
  },
  origin() {
    return this._origin.slice();
  },
  signal(name, value, options) {
    const op = lookupSignal(this, name);
    return arguments.length === 1 ? op.value : this.update(op, value, options);
  },
  width(_) {
    return arguments.length ? this.signal('width', _) : this.signal('width');
  },
  height(_) {
    return arguments.length ? this.signal('height', _) : this.signal('height');
  },
  padding(_) {
    return arguments.length ? this.signal('padding', padding$1(_)) : padding$1(this.signal('padding'));
  },
  autosize(_) {
    return arguments.length ? this.signal('autosize', _) : this.signal('autosize');
  },
  background(_) {
    return arguments.length ? this.signal('background', _) : this.signal('background');
  },
  renderer(type) {
    if (!arguments.length) return this._renderType;
    if (!renderModule(type)) error$1('Unrecognized renderer type: ' + type);
    if (type !== this._renderType) {
      this._renderType = type;
      this._resetRenderer();
    }
    return this;
  },
  tooltip(handler) {
    if (!arguments.length) return this._tooltip;
    if (handler !== this._tooltip) {
      this._tooltip = handler;
      this._resetRenderer();
    }
    return this;
  },
  loader(loader) {
    if (!arguments.length) return this._loader;
    if (loader !== this._loader) {
      Dataflow.prototype.loader.call(this, loader);
      this._resetRenderer();
    }
    return this;
  },
  resize() {
    // set flag to perform autosize
    this._autosize = 1;
    // touch autosize signal to ensure top-level ViewLayout runs
    return this.touch(lookupSignal(this, 'autosize'));
  },
  _resetRenderer() {
    if (this._renderer) {
      this._renderer = null;
      this.initialize(this._el, this._elBind);
    }
  },
  // -- SIZING ----
  _resizeView: resizeView,
  // -- EVENT HANDLING ----

  addEventListener(type, handler, options) {
    let callback = handler;
    if (!(options && options.trap === false)) {
      // wrap callback in error handler
      callback = trap(this, handler);
      callback.raw = handler;
    }
    this._handler.on(type, callback);
    return this;
  },
  removeEventListener(type, handler) {
    var handlers = this._handler.handlers(type),
      i = handlers.length,
      h,
      t;

    // search registered handlers, remove if match found
    while (--i >= 0) {
      t = handlers[i].type;
      h = handlers[i].handler;
      if (type === t && (handler === h || handler === h.raw)) {
        this._handler.off(t, h);
        break;
      }
    }
    return this;
  },
  addResizeListener(handler) {
    const l = this._resizeListeners;
    if (!l.includes(handler)) {
      // add handler if it isn't already registered
      // note: error trapping handled elsewhere, so
      // no need to wrap handlers here
      l.push(handler);
    }
    return this;
  },
  removeResizeListener(handler) {
    var l = this._resizeListeners,
      i = l.indexOf(handler);
    if (i >= 0) {
      l.splice(i, 1);
    }
    return this;
  },
  addSignalListener(name, handler) {
    return addOperatorListener(this, name, lookupSignal(this, name), handler);
  },
  removeSignalListener(name, handler) {
    return removeOperatorListener(this, lookupSignal(this, name), handler);
  },
  addDataListener(name, handler) {
    return addOperatorListener(this, name, dataref(this, name).values, handler);
  },
  removeDataListener(name, handler) {
    return removeOperatorListener(this, dataref(this, name).values, handler);
  },
  globalCursor(_) {
    if (arguments.length) {
      if (this._globalCursor !== !!_) {
        const prev = setCursor(this, null); // clear previous cursor
        this._globalCursor = !!_;
        if (prev) setCursor(this, prev); // swap cursor
      }
      return this;
    } else {
      return this._globalCursor;
    }
  },
  preventDefault(_) {
    if (arguments.length) {
      this._preventDefault = _;
      return this;
    } else {
      return this._preventDefault;
    }
  },
  timer,
  events,
  finalize,
  hover,
  // -- DATA ----
  data,
  change,
  insert,
  remove,
  // -- SCALES --
  scale: scale$1,
  // -- INITIALIZATION ----
  initialize,
  // -- HEADLESS RENDERING ----
  toImageURL: renderToImageURL,
  toCanvas: renderToCanvas,
  toSVG: renderToSVG,
  // -- SAVE / RESTORE STATE ----
  getState,
  setState,
  // RE-RENDER ON ZOOM
  _watchPixelRatio: watchPixelRatio
});

const VIEW = 'view',
  LBRACK = '[',
  RBRACK = ']',
  LBRACE = '{',
  RBRACE = '}',
  COLON = ':',
  COMMA = ',',
  NAME = '@',
  GT = '>',
  ILLEGAL = /[[\]{}]/,
  DEFAULT_MARKS = {
    '*': 1,
    arc: 1,
    area: 1,
    group: 1,
    image: 1,
    line: 1,
    path: 1,
    rect: 1,
    rule: 1,
    shape: 1,
    symbol: 1,
    text: 1,
    trail: 1
  };
let DEFAULT_SOURCE, MARKS;

/**
 * Parse an event selector string.
 * Returns an array of event stream definitions.
 */
function eventSelector (selector, source, marks) {
  DEFAULT_SOURCE = source || VIEW;
  MARKS = marks || DEFAULT_MARKS;
  return parseMerge(selector.trim()).map(parseSelector);
}
function isMarkType(type) {
  return MARKS[type];
}
function find(s, i, endChar, pushChar, popChar) {
  const n = s.length;
  let count = 0,
    c;
  for (; i < n; ++i) {
    c = s[i];
    if (!count && c === endChar) return i;else if (popChar && popChar.includes(c)) --count;else if (pushChar && pushChar.includes(c)) ++count;
  }
  return i;
}
function parseMerge(s) {
  const output = [],
    n = s.length;
  let start = 0,
    i = 0;
  while (i < n) {
    i = find(s, i, COMMA, LBRACK + LBRACE, RBRACK + RBRACE);
    output.push(s.substring(start, i).trim());
    start = ++i;
  }
  if (output.length === 0) {
    throw 'Empty event selector: ' + s;
  }
  return output;
}
function parseSelector(s) {
  return s[0] === '[' ? parseBetween(s) : parseStream$1(s);
}
function parseBetween(s) {
  const n = s.length;
  let i = 1,
    b;
  i = find(s, i, RBRACK, LBRACK, RBRACK);
  if (i === n) {
    throw 'Empty between selector: ' + s;
  }
  b = parseMerge(s.substring(1, i));
  if (b.length !== 2) {
    throw 'Between selector must have two elements: ' + s;
  }
  s = s.slice(i + 1).trim();
  if (s[0] !== GT) {
    throw 'Expected \'>\' after between selector: ' + s;
  }
  b = b.map(parseSelector);
  const stream = parseSelector(s.slice(1).trim());
  if (stream.between) {
    return {
      between: b,
      stream: stream
    };
  } else {
    stream.between = b;
  }
  return stream;
}
function parseStream$1(s) {
  const stream = {
      source: DEFAULT_SOURCE
    },
    source = [];
  let throttle = [0, 0],
    markname = 0,
    start = 0,
    n = s.length,
    i = 0,
    j,
    filter;

  // extract throttle from end
  if (s[n - 1] === RBRACE) {
    i = s.lastIndexOf(LBRACE);
    if (i >= 0) {
      try {
        throttle = parseThrottle(s.substring(i + 1, n - 1));
      } catch (e) {
        throw 'Invalid throttle specification: ' + s;
      }
      s = s.slice(0, i).trim();
      n = s.length;
    } else throw 'Unmatched right brace: ' + s;
    i = 0;
  }
  if (!n) throw s;

  // set name flag based on first char
  if (s[0] === NAME) markname = ++i;

  // extract first part of multi-part stream selector
  j = find(s, i, COLON);
  if (j < n) {
    source.push(s.substring(start, j).trim());
    start = i = ++j;
  }

  // extract remaining part of stream selector
  i = find(s, i, LBRACK);
  if (i === n) {
    source.push(s.substring(start, n).trim());
  } else {
    source.push(s.substring(start, i).trim());
    filter = [];
    start = ++i;
    if (start === n) throw 'Unmatched left bracket: ' + s;
  }

  // extract filters
  while (i < n) {
    i = find(s, i, RBRACK);
    if (i === n) throw 'Unmatched left bracket: ' + s;
    filter.push(s.substring(start, i).trim());
    if (i < n - 1 && s[++i] !== LBRACK) throw 'Expected left bracket: ' + s;
    start = ++i;
  }

  // marshall event stream specification
  if (!(n = source.length) || ILLEGAL.test(source[n - 1])) {
    throw 'Invalid event selector: ' + s;
  }
  if (n > 1) {
    stream.type = source[1];
    if (markname) {
      stream.markname = source[0].slice(1);
    } else if (isMarkType(source[0])) {
      stream.marktype = source[0];
    } else {
      stream.source = source[0];
    }
  } else {
    stream.type = source[0];
  }
  if (stream.type.slice(-1) === '!') {
    stream.consume = true;
    stream.type = stream.type.slice(0, -1);
  }
  if (filter != null) stream.filter = filter;
  if (throttle[0]) stream.throttle = throttle[0];
  if (throttle[1]) stream.debounce = throttle[1];
  return stream;
}
function parseThrottle(s) {
  const a = s.split(COMMA);
  if (!s.length || a.length > 2) throw s;
  return a.map(_ => {
    const x = +_;
    if (x !== x) throw s;
    return x;
  });
}

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

const number = _ => +_ || 0;
const paddingObject = _ => ({
  top: _,
  bottom: _,
  left: _,
  right: _
});
function parsePadding (spec) {
  return !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 = _ => isObject(_) && !isArray(_) ? extend({}, _) : {
  value: _
};
function addEncode(object, name, value, set) {
  if (value != null) {
    const isEncoder = isObject(value) && !isArray(value) || isArray(value) && value.length && isObject(value[0]);

    // Always assign signal to update, even if the signal is from the enter block
    if (isEncoder) {
      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 && has$1(skip, name)) continue;
    encode[name] = extend(encode[name] || {}, extra[name]);
  }
  return encode;
}
function has(key, encode) {
  return encode && (encode.enter && encode.enter[key] || encode.update && encode.update[key]);
}

const MarkRole = 'mark';
const FrameRole = 'frame';
const ScopeRole = 'scope';
const AxisRole = 'axis';
const AxisDomainRole = 'axis-domain';
const AxisGridRole = 'axis-grid';
const AxisLabelRole = 'axis-label';
const AxisTickRole = 'axis-tick';
const AxisTitleRole = 'axis-title';
const LegendRole = 'legend';
const LegendBandRole = 'legend-band';
const LegendEntryRole = 'legend-entry';
const LegendGradientRole = 'legend-gradient';
const LegendLabelRole = 'legend-label';
const LegendSymbolRole = 'legend-symbol';
const LegendTitleRole = 'legend-title';
const TitleRole = 'title';
const TitleTextRole = 'title-text';
const 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 ? 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
  array$2(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 = extend({}, encode); // defensive copy
  for (key in defaults) {
    props = defaults[key];
    if (props.signal) {
      (update = update || {})[key] = props;
    } else {
      enter[key] = props;
    }
  }
  encode.enter = extend(enter, encode.enter);
  if (update) encode.update = extend(update, encode.update);
  return encode;
}
function applyDefault(defaults, key, value) {
  defaults[key] = value && value.signal ? {
    signal: value.signal
  } : {
    value: value
  };
}

const scaleRef = scale => isString(scale) ? $(scale) : scale.signal ? `(${scale.signal})` : field(scale);
function entry$1(enc) {
  if (enc.gradient != null) {
    return gradient$1(enc);
  }
  let value = enc.signal ? `(${enc.signal})` : enc.color ? color$1(enc.color) : enc.field != null ? field(enc.field) : enc.value !== undefined ? $(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$1).join(',')})+'')`;
function color$1(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$1(enc) {
  // map undefined to null; expression lang does not allow undefined
  const args = [enc.start, enc.stop, enc.count].map(_ => _ == null ? null : $(_));

  // trim null inputs from the end
  while (args.length && peek$1(args) == null) args.pop();
  args.unshift(scaleRef(enc.gradient));
  return `gradient(${args.join(',')})`;
}
function property(property) {
  return isObject(property) ? '(' + entry$1(property) + ')' : property;
}
function field(ref) {
  return resolveField(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 {
    error$1('Invalid field reference: ' + $(ref));
  }
  if (!ref.signal) {
    field = isString(field) ? splitAccessPath(field).map($).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$1 (enc) {
  let code = '';
  enc.forEach(rule => {
    const value = entry$1(rule);
    code += rule.test ? `(${rule.test})?${value}:` : value;
  });

  // if no else clause, terminate with null (#1366)
  if (peek$1(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$1(expr(block[name]), scope, params, fields);
    }
  }
  return {
    $expr: {
      marktype,
      channels
    },
    $fields: Object.keys(fields),
    $output: Object.keys(block)
  };
}
function expr(enc) {
  return isArray(enc) ? rule$1(enc) : entry$1(enc);
}
function parse$1(code, scope, params, fields) {
  const expr = parser(code, scope);
  expr.$fields.forEach(name => fields[name] = 1);
  extend(params, expr.$params);
  return expr.$expr;
}

const OUTER = 'outer',
  OUTER_INVALID = ['value', 'update', 'init', 'react', 'bind'];
function outerError(prefix, name) {
  error$1(prefix + ' for "outer" push: ' + $(name));
}
function parseSignal (signal, scope) {
  const 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(prop => {
      if (signal[prop] !== undefined) outerError('Invalid property ', prop);
    });
  } else {
    // define a new signal in the current scope
    const 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(type, value, params, parent) {
  return new Entry(type, value, params, parent);
}
function operator(value, params) {
  return entry('operator', value, params);
}

// -----

function ref(op) {
  const 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$1(field, name) {
  return name ? {
    $field: field,
    $name: name
  } : {
    $field: field
  };
}
const keyFieldRef = fieldRef$1('key');
function compareRef(fields, orders) {
  return {
    $compare: fields,
    $order: orders
  };
}
function keyRef(fields, flat) {
  const ref = {
    $key: fields
  };
  if (flat) ref.$flat = true;
  return ref;
}

// -----

const Ascending = 'ascending';
const Descending = 'descending';
function sortKey(sort) {
  return !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 || '');
}

// -----

const Scope$1 = 'scope';
const View = 'view';
function isSignal(_) {
  return _ && _.signal;
}
function isExpr$1(_) {
  return _ && _.expr;
}
function hasSignal(_) {
  if (isSignal(_)) return true;
  if (isObject(_)) for (const 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;
}

const Timer = 'timer';
function parseStream(stream, scope) {
  const method = stream.merge ? mergeStream : stream.stream ? nestedStream : stream.type ? eventStream : error$1('Invalid stream specification: ' + $(stream));
  return method(stream, scope);
}
function eventSource(source) {
  return source === Scope$1 ? View : source || View;
}
function mergeStream(stream, scope) {
  const list = stream.merge.map(s => parseStream(s, scope)),
    entry = streamParameters({
      merge: list
    }, stream, scope);
  return scope.addStream(entry).id;
}
function nestedStream(stream, scope) {
  const id = parseStream(stream.stream, scope),
    entry = streamParameters({
      stream: id
    }, stream, scope);
  return scope.addStream(entry).id;
}
function eventStream(stream, scope) {
  let id;
  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);
  }
  const entry = streamParameters({
    stream: id
  }, stream, scope);
  return Object.keys(entry).length === 1 ? id : scope.addStream(entry).id;
}
function streamParameters(entry, stream, scope) {
  let param = stream.between;
  if (param) {
    if (param.length !== 2) {
      error$1('Stream "between" parameter must have 2 entries: ' + $(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$1) {
    // add filter to limit events from sub-scope only
    param.push('inScope(event.item)');
  }
  if (param.length) {
    entry.filter = parser('(' + 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) {
  const 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) {
  const encode = spec.encode,
    entry = {
      target: target
    };
  let events = spec.events,
    update = spec.update,
    sources = [];
  if (!events) {
    error$1('Signal update missing events specification.');
  }

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

  // separate event streams from signal updates
  events = array$2(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) error$1('Signal encode and update are mutually exclusive.');
    update = 'encode(item(),' + $(encode) + ')';
  }

  // resolve update value
  entry.update = isString(update) ? parser(update, scope) : update.expr != null ? parser(update.expr, scope) : update.value != null ? update.value : update.signal != null ? {
    $expr: OP_VALUE_EXPR,
    $params: {
      $value: scope.signalRef(update.signal)
    }
  } : error$1('Invalid signal update specification.');
  if (spec.force) {
    entry.options = {
      force: true
    };
  }
  sources.forEach(source => scope.addUpdate(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) {
  const op = scope.getSignal(signal.name);
  let expr = signal.update;
  if (signal.init) {
    if (expr) {
      error$1('Signals can not include both init and update expressions.');
    } else {
      expr = signal.init;
      op.initonly = true;
    }
  }
  if (expr) {
    expr = parser(expr, scope);
    op.update = expr.$expr;
    op.params = expr.$params;
  }
  if (signal.on) {
    signal.on.forEach(_ => parseUpdate(_, scope, op.id));
  }
}

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

let FIELD_REF_ID = 0;
const MULTIDOMAIN_SORT_OPS = {
  min: 'min',
  max: 'max',
  count: 'sum'
};
function initScale(spec, scope) {
  const type = spec.type || 'linear';
  if (!isValidScaleType(type)) {
    error$1('Unrecognized scale type: ' + $(type));
  }
  scope.addScale(spec.name, {
    type,
    domain: undefined
  });
}
function parseScale(spec, scope) {
  const params = scope.getScale(spec.name).params;
  let key;
  params.domain = parseScaleDomain$1(spec.domain, spec, scope);
  if (spec.range != null) {
    params.range = parseScaleRange$1(spec, scope, params);
  }
  if (spec.interpolate != null) {
    parseScaleInterpolate(spec.interpolate, params);
  }
  if (spec.nice != null) {
    params.nice = parseScaleNice(spec.nice, scope);
  }
  if (spec.bins != null) {
    params.bins = parseScaleBins(spec.bins, scope);
  }
  for (key in spec) {
    if (has$1(params, key) || key === 'name') continue;
    params[key] = parseLiteral(spec[key], scope);
  }
}
function parseLiteral(v, scope) {
  return !isObject(v) ? v : v.signal ? scope.signalRef(v.signal) : error$1('Unsupported object: ' + $(v));
}
function parseArray(v, scope) {
  return v.signal ? scope.signalRef(v.signal) : v.map(v => parseLiteral(v, scope));
}
function dataLookupError(name) {
  error$1('Can not find data set: ' + $(name));
}

// -- SCALE DOMAIN ----

function parseScaleDomain$1(domain, spec, scope) {
  if (!domain) {
    if (spec.domainMin != null || spec.domainMax != null) {
      error$1('No scale domain defined for domainMin/domainMax to override.');
    }
    return; // default domain
  }
  return domain.signal ? scope.signalRef(domain.signal) : (isArray(domain) ? explicitDomain : domain.fields ? multipleDomain : singularDomain)(domain, spec, scope);
}
function explicitDomain(domain, spec, scope) {
  return domain.map(v => parseLiteral(v, scope));
}
function singularDomain(domain, spec, scope) {
  const data = scope.getData(domain.data);
  if (!data) dataLookupError(domain.data);
  return isDiscrete$2(spec.type) ? data.valuesRef(scope, domain.field, parseSort(domain.sort, false)) : isQuantile$1(spec.type) ? data.domainRef(scope, domain.field) : data.extentRef(scope, domain.field);
}
function multipleDomain(domain, spec, scope) {
  const data = domain.data,
    fields = domain.fields.reduce((dom, d) => {
      d = isString(d) ? {
        data: data,
        field: d
      } : isArray(d) || d.signal ? fieldRef(d, scope) : d;
      dom.push(d);
      return dom;
    }, []);
  return (isDiscrete$2(spec.type) ? ordinalMultipleDomain : isQuantile$1(spec.type) ? quantileMultipleDomain : numericMultipleDomain)(domain, scope, fields);
}
function fieldRef(data, scope) {
  const name = '_:vega:_' + FIELD_REF_ID++,
    coll = Collect({});
  if (isArray(data)) {
    coll.value = {
      $ingest: data
    };
  } else if (data.signal) {
    const code = 'setdata(' + $(name) + ',' + data.signal + ')';
    coll.params.input = scope.signalRef(code);
  }
  scope.addDataPipeline(name, [coll, Sieve({})]);
  return {
    data: name,
    field: 'data'
  };
}
function ordinalMultipleDomain(domain, scope, fields) {
  const sort = parseSort(domain.sort, true);
  let a, v;

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

  // aggregate the results from each domain field
  const 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
  const 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 (isObject(sort)) sort.field = 'key';else sort = {
        field: 'key'
      };
    } else if (!sort.field && sort.op !== 'count') {
      error$1('No field provided for sort aggregate op: ' + sort.op);
    } else if (multidomain && sort.field) {
      if (sort.op && !MULTIDOMAIN_SORT_OPS[sort.op]) {
        error$1('Multiple domain scales can not be sorted using ' + sort.op);
      }
    }
  }
  return sort;
}
function quantileMultipleDomain(domain, scope, fields) {
  // get value arrays for each domain field
  const values = fields.map(f => {
    const 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
  const extents = fields.map(f => {
    const 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 || isArray(v) ? parseArray(v, scope) : scope.objectProperty(v);
}

// -- SCALE NICE -----

function parseScaleNice(nice, scope) {
  return nice.signal ? scope.signalRef(nice.signal) : 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$1(spec, scope, params) {
  const config = scope.config.range;
  let range = spec.range;
  if (range.signal) {
    return scope.signalRef(range.signal);
  } else if (isString(range)) {
    if (config && has$1(config, range)) {
      spec = extend({}, spec, {
        range: config[range]
      });
      return parseScaleRange$1(spec, scope, params);
    } else if (range === 'width') {
      range = [0, {
        signal: 'width'
      }];
    } else if (range === 'height') {
      range = isDiscrete$2(spec.type) ? [0, {
        signal: 'height'
      }] : [{
        signal: 'height'
      }, 0];
    } else {
      error$1('Unrecognized scale range value: ' + $(range));
    }
  } else if (range.scheme) {
    params.scheme = 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 (isDiscrete$2(spec.type) && !isArray(range)) {
    return parseScaleDomain$1(range, spec, scope);
  } else if (!isArray(range)) {
    error$1('Unsupported range type: ' + $(range));
  }
  return range.map(v => (isArray(v) ? parseArray : parseLiteral)(v, scope));
}

function parseProjection$1 (proj, scope) {
  const config = scope.config.projection || {},
    params = {};
  for (const name in proj) {
    if (name === 'name') continue;
    params[name] = parseParameter$1(proj[name], name, scope);
  }

  // apply projection defaults from config
  for (const name in config) {
    if (params[name] == null) {
      params[name] = parseParameter$1(config[name], name, scope);
    }
  }
  scope.addProjection(proj.name, params);
}
function parseParameter$1(_, name, scope) {
  return isArray(_) ? _.map(_ => parseParameter$1(_, name, scope)) : !isObject(_) ? _ : _.signal ? scope.signalRef(_.signal) : name === 'fit' ? _ : error$1('Unsupported parameter object: ' + $(_));
}

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

/** All values of LegendType */
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$1 = {
  value: 0
};
const one = {
  value: 1
};

const GroupMark = 'group';
const RectMark = 'rect';
const RuleMark = 'rule';
const SymbolMark = 'symbol';
const 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) {
  const 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) {
  const 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$1 = anchorExpr($(Left), $(Right), $(Center));
function tickBand(_) {
  const v = _('tickBand');
  let 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 (!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 : !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) {
  const _ = lookup(spec, config),
    vertical = _.isVertical(),
    thickness = _.gradientThickness(),
    length = _.gradientLength();
  let 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;
  }
  const encode = {
    enter: enter = {
      opacity: zero$1,
      x: zero$1,
      y: zero$1,
      width: encoder(width),
      height: encoder(height)
    },
    update: extend({}, enter, {
      opacity: one,
      fill: {
        gradient: scale,
        start: start,
        stop: stop
      }
    }),
    exit: {
      opacity: zero$1
    }
  };
  addEncoders(encode, {
    stroke: _('gradientStrokeColor'),
    strokeWidth: _('gradientStrokeWidth')
  }, {
    // update
    opacity: _('gradientOpacity')
  });
  return guideMark({
    type: RectMark,
    role: LegendGradientRole,
    encode
  }, userEncode);
}

function legendGradientDiscrete (spec, scale, config, userEncode, dataRef) {
  const _ = lookup(spec, config),
    vertical = _.isVertical(),
    thickness = _.gradientThickness(),
    length = _.gradientLength();
  let u,
    v,
    uu,
    vv,
    adjust = '';
  vertical ? (u = 'y', uu = 'y2', v = 'x', vv = 'width', adjust = '1-') : (u = 'x', uu = 'x2', v = 'y', vv = 'height');
  const enter = {
    opacity: zero$1,
    fill: {
      scale: scale,
      field: Value
    }
  };
  enter[u] = {
    signal: adjust + 'datum.' + Perc,
    mult: length
  };
  enter[v] = zero$1;
  enter[uu] = {
    signal: adjust + 'datum.' + Perc2,
    mult: length
  };
  enter[vv] = encoder(thickness);
  const encode = {
    enter: enter,
    update: extend({}, enter, {
      opacity: one
    }),
    exit: {
      opacity: zero$1
    }
  };
  addEncoders(encode, {
    stroke: _('gradientStrokeColor'),
    strokeWidth: _('gradientStrokeWidth')
  }, {
    // update
    opacity: _('gradientOpacity')
  });
  return guideMark({
    type: RectMark,
    role: LegendBandRole,
    key: Value,
    from: dataRef,
    encode
  }, userEncode);
}

const alignExpr = `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) {
  const _ = lookup(spec, config),
    vertical = _.isVertical(),
    thickness = encoder(_.gradientThickness()),
    length = _.gradientLength();
  let overlap = _('labelOverlap'),
    enter,
    update,
    u,
    v,
    adjust = '';
  const encode = {
    enter: enter = {
      opacity: zero$1
    },
    update: update = {
      opacity: one,
      text: {
        field: Label
      }
    },
    exit: {
      opacity: zero$1
    }
  };
  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
    };
    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) {
  const _ = lookup(spec, config),
    entries = userEncode.entries,
    interactive = !!(entries && entries.interactive),
    name = entries ? entries.name : undefined,
    height = _('clipHeight'),
    symbolOffset = _('symbolOffset'),
    valueRef = {
      data: 'value'
    },
    xSignal = `(${columns}) ? datum.${Offset} : datum.${Size}`,
    yEncode = height ? encoder(height) : {
      field: Size
    },
    index = `datum.${Index}`,
    ncols = `max(1, ${columns})`;
  let encode, enter, update, nrows, sort;
  yEncode.mult = 0.5;

  // -- LEGEND SYMBOLS --
  encode = {
    enter: enter = {
      opacity: zero$1,
      x: {
        signal: xSignal,
        mult: 0.5,
        offset: symbolOffset
      },
      y: yEncode
    },
    update: update = {
      opacity: one,
      x: enter.x,
      y: enter.y
    },
    exit: {
      opacity: zero$1
    }
  };
  let 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(scale => {
    if (spec[scale]) {
      update[scale] = enter[scale] = {
        scale: spec[scale],
        field: Value
      };
    }
  });
  const symbols = guideMark({
    type: SymbolMark,
    role: LegendSymbolRole,
    key: Value,
    from: valueRef,
    clip: height ? true : undefined,
    encode
  }, userEncode.symbols);

  // -- LEGEND LABELS --
  const labelOffset = encoder(symbolOffset);
  labelOffset.offset = _('labelOffset');
  encode = {
    enter: enter = {
      opacity: zero$1,
      x: {
        signal: xSignal,
        offset: labelOffset
      },
      y: yEncode
    },
    update: update = {
      opacity: one,
      text: {
        field: Label
      },
      x: enter.x,
      y: enter.y
    },
    exit: {
      opacity: zero$1
    }
  };
  addEncoders(encode, {
    align: _('labelAlign'),
    baseline: _('labelBaseline'),
    fill: _('labelColor'),
    fillOpacity: _('labelOpacity'),
    font: _('labelFont'),
    fontSize: _('labelFontSize'),
    fontStyle: _('labelFontStyle'),
    fontWeight: _('labelFontWeight'),
    limit: _('labelLimit')
  });
  const 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$1,
      height: height ? encoder(height) : zero$1,
      opacity: zero$1
    },
    exit: {
      opacity: zero$1
    },
    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$1 = anchorExpr('"top"', '"bottom"', '"middle"'),
  alignFlip = anchorExpr('"right"', '"left"', '"center"'),
  exprAlign = `datum.vgrad && ${isR} ? (${alignFlip}) : (${isLR} && !(datum.vgrad && ${isL})) ? "left" : ${alignExpr$1}`,
  exprAnchor = `item._anchor || (${isLR} ? "middle" : "start")`,
  exprAngle = `${isVG} ? (${isL} ? -90 : 90) : 0`,
  exprBaseline = `${isLR} ? (datum.vgrad ? (${isR} ? "bottom" : "top") : ${baseline$1}) : "top"`;
function legendTitle (spec, config, userEncode, dataRef) {
  const _ = lookup(spec, config);
  const encode = {
    enter: {
      opacity: zero$1
    },
    update: {
      opacity: one,
      x: {
        field: {
          group: 'padding'
        }
      },
      y: {
        field: {
          group: 'padding'
        }
      }
    },
    exit: {
      opacity: zero$1
    }
  };
  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) {
  let expr;
  if (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 isObject(value) && value.signal ? value.signal : $(value);
}

function getRole (spec) {
  const role = spec.role || '';
  return role.startsWith('axis') || role.startsWith('legend') || role.startsWith('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) {
  const def = definition$1(spec.type);
  if (!def) error$1('Unrecognized transform type: ' + $(spec.type));
  const t = entry(def.type.toLowerCase(), null, parseParameters(def, spec, scope));
  if (spec.signal) scope.addSignal(spec.signal, scope.proxy(t));
  t.metadata = def.metadata || {};
  return t;
}

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

/**
 * Parse a data transform parameter.
 */
function parseParameter(def, spec, scope) {
  const type = def.type,
    value = spec[def.name];
  if (type === 'index') {
    return parseIndexParameter(def, spec, scope);
  } else if (value === undefined) {
    if (def.required) {
      error$1('Missing required ' + $(spec.type) + ' parameter: ' + $(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(v => parameterValue(def, v, scope)) : parameterValue(def, value, scope);
}

/**
 * Parse a single parameter value.
 */
function parameterValue(def, value, scope) {
  const type = def.type;
  if (isSignal(value)) {
    return isExpr(type) ? error$1('Expression references can not be signals.') : isField(type) ? scope.fieldRef(value) : isCompare(type) ? scope.compareRef(value) : scope.signalRef(value.signal);
  } else {
    const expr = def.expr || isField(type);
    return expr && outerExpr(value) ? scope.exprRef(value.expr, value.as) : expr && outerField(value) ? fieldRef$1(value.field, value.as) : isExpr(type) ? parser(value, scope) : isData(type) ? ref(scope.getData(value).values) : isField(type) ? fieldRef$1(value) : isCompare(type) ? scope.compareRef(value) : value;
  }
}

/**
 * Parse parameter for accessing an index of another data set.
 */
function parseIndexParameter(def, spec, scope) {
  if (!isString(spec.from)) {
    error$1('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) {
  const value = spec[def.name];
  if (def.array) {
    if (!isArray(value)) {
      // signals not allowed!
      error$1('Expected an array of sub-parameters. Instead: ' + $(value));
    }
    return value.map(v => parseSubParameter(def, v, scope));
  } else {
    return parseSubParameter(def, value, scope);
  }
}

/**
 * Parse a sub-parameter object.
 */
function parseSubParameter(def, value, scope) {
  const n = def.params.length;
  let pdef;

  // loop over defs to find matching key
  for (let i = 0; i < n; ++i) {
    pdef = def.params[i];
    for (const 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) error$1('Unsupported parameter: ' + $(value));

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

// -- Utilities -----

const outerExpr = _ => _ && _.expr;
const outerField = _ => _ && _.field;
const isData = _ => _ === 'data';
const isExpr = _ => _ === 'expr';
const isField = _ => _ === 'field';
const isCompare = _ => _ === 'compare';

function parseData$1 (from, group, scope) {
  let 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) error$1('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(extend({
          type: 'aggregate',
          groupby: array$2(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) {
  const n = entries.length,
    values = entries[n - 1],
    output = entries[n - 2];
  let input = entries[0],
    aggr = null,
    i = 1;
  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);
};
function fieldKey(field) {
  return isString(field) ? field : null;
}
function addSortField(scope, p, sort) {
  const as = aggrField(sort.op, sort.field);
  let s;
  if (p.ops) {
    for (let 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) {
  const cache = ds[name] || (ds[name] = {}),
    sort = sortKey(counts);
  let k = fieldKey(field),
    v,
    op;
  if (k != null) {
    scope = ds.scope;
    k = k + (sort ? '|' + sort : '');
    v = cache[k];
  }
  if (!v) {
    const params = counts ? {
      field: keyFieldRef,
      pulse: ds.countsRef(scope, field, counts)
    } : {
      field: scope.fieldRef(field),
      pulse: ref(ds.output)
    };
    if (sort) params.sort = scope.sortRef(counts);
    op = scope.add(entry(optype, undefined, params));
    if (index) ds.index[field] = op;
    v = ref(op);
    if (k != null) cache[k] = v;
  }
  return v;
}
DataScope.prototype = {
  countsRef(scope, field, sort) {
    const ds = this,
      cache = ds.counts || (ds.counts = {}),
      k = fieldKey(field);
    let 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;
  },
  tuplesRef() {
    return ref(this.values);
  },
  extentRef(scope, field) {
    return cache(scope, this, 'extent', 'extent', field, false);
  },
  domainRef(scope, field) {
    return cache(scope, this, 'domain', 'values', field, false);
  },
  valuesRef(scope, field, sort) {
    return cache(scope, this, 'vals', 'values', field, sort || true);
  },
  lookupRef(scope, field) {
    return cache(scope, this, 'lookup', 'tupleindex', field, false);
  },
  indataRef(scope, field) {
    return cache(scope, this, 'indata', 'tupleindex', field, true, true);
  }
};

function parseFacet (spec, scope, group) {
  const facet = spec.from.facet,
    name = facet.name,
    data = getDataRef(facet, scope);
  let op;
  if (!facet.name) {
    error$1('Facet must have a name: ' + $(facet));
  }
  if (!facet.data) {
    error$1('Facet must reference a data set: ' + $(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 {
    error$1('Facet must specify groupby or field: ' + $(facet));
  }

  // initialize facet subscope
  const 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) {
  const 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) {
  const remove = spec.remove,
    insert = spec.insert,
    toggle = spec.toggle,
    modify = spec.modify,
    values = spec.values,
    op = scope.add(operator());
  const update = 'if(' + spec.trigger + ',modify("' + name + '",' + [insert, remove, toggle, modify, values].map(_ => _ == null ? 'null' : _).join(',') + '),0)';
  const expr = parser(update, scope);
  op.update = expr.$expr;
  op.params = expr.$params;
}

function parseMark (spec, scope) {
  const role = getRole(spec),
    group = spec.type === GroupMark,
    facet = spec.from && spec.from.facet,
    overlap = spec.overlap;
  let layout = spec.layout || role === ScopeRole || role === FrameRole,
    ops,
    op,
    store,
    enc,
    name,
    layoutRef,
    boundRef;
  const nested = role === MarkRole || layout || facet;

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

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

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

  // connect visual items to scenegraph
  op = scope.add(Mark$1({
    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)
  }));
  const 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(_ => {
      const tx = parseTransform(_, scope),
        md = tx.metadata;
      if (md.generates || md.changes) {
        error$1('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)
    }));
  }
  const 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
  const 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
  const 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(on => {
      if (on.insert || on.remove || on.toggle) {
        error$1('Marks only support modify triggers.');
      }
      parseTrigger(on, scope, name);
    });
  }
}
function parseOverlap(overlap, source, scope) {
  const method = overlap.method,
    bound = overlap.bound,
    sep = overlap.separation;
  const 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) {
    const 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$1 (spec, scope) {
  const config = scope.config.legend,
    encode = spec.encode || {},
    _ = lookup(spec, config),
    legendEncode = encode.legend || {},
    name = legendEncode.name || undefined,
    interactive = legendEncode.interactive,
    style = legendEncode.style,
    scales = {};
  let scale = 0,
    entryLayout,
    params,
    children;

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

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

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

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

  // data source for legend values
  const 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: extendEncode(buildLegendEncode(_, spec, config), legendEncode, Skip),
    marks: children,
    aria: _('aria'),
    description: _('description'),
    zindex: _('zindex'),
    name,
    interactive,
    style
  }), scope);
}
function legendType(spec, scaleType) {
  let type = spec.type || Symbols;
  if (!spec.type && scaleCount(spec) === 1 && (spec.fill || spec.stroke)) {
    type = isContinuous$1(scaleType) ? Gradient : isDiscretizing$1(scaleType) ? Discrete : Symbols;
  }
  return type !== Gradient ? type : isDiscretizing$1(scaleType) ? Discrete : Gradient;
}
function scaleCount(spec) {
  return LegendScales.reduce((count, type) => count + (spec[type] ? 1 : 0), 0);
}
function buildLegendEncode(_, spec, config) {
  const 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) {
  const size = deref(getChannel('size', spec, marks)),
    strokeWidth = deref(getChannel('strokeWidth', spec, marks)),
    fontSize = deref(getFontSize(marks[1].encode, scope, GuideLabelStyle));
  return parser(`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 = isString(spec) ? {
    text: spec
  } : spec;
  const _ = lookup(spec, scope.config.title),
    encode = spec.encode || {},
    userEncode = encode.group || {},
    name = userEncode.name || undefined,
    interactive = userEncode.interactive,
    style = userEncode.style,
    children = [];

  // single-element data source for group title
  const 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 || extend({
    name: spec.name,
    interactive: spec.interactive,
    style: spec.style
  }, encode);
}
function groupEncode(_, userEncode) {
  const encode = {
    enter: {},
    update: {}
  };
  addEncoders(encode, {
    orient: _('orient'),
    anchor: _('anchor'),
    align: {
      signal: alignExpr$1
    },
    angle: {
      signal: angleExpr
    },
    limit: _('limit'),
    frame: _('frame'),
    offset: _('offset') || 0,
    padding: _('subtitlePadding')
  });
  return extendEncode(encode, userEncode, Skip);
}
function buildTitle(spec, _, userEncode, dataRef) {
  const 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) {
  const 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$2(data, scope) {
  const transforms = [];
  if (data.transform) {
    data.transform.forEach(tx => {
      transforms.push(parseTransform(tx, scope));
    });
  }
  if (data.on) {
    data.on.forEach(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) {
  const output = [];
  let source = null,
    modify = false,
    generate = false,
    upstream,
    i,
    n,
    t,
    m;
  if (data.values) {
    // hard-wired input data set
    if (isSignal(data.values) || hasSignal(data.format)) {
      // if either values is signal 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 = array$2(data.source).map(d => 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) {
  const 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 || $(a.value) : null;
    b = b ? b.signal || $(b.value) : null;
    return {
      signal: `${test} ? (${a}) : (${b})`
    };
  } else {
    // otherwise generate rule set
    return [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 : $(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 && has$1(config, prop)) {
    return config[prop];
  } else if (has$1(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$1(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$1([config.axisX, config.axisY]),
      orientKeys = keys$1([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)];
  }
  const result = xy || or || band ? extend({}, axis, xy, or, band) : axis;
  return result;
}

function axisDomain (spec, config, userEncode, dataRef) {
  const _ = lookup(spec, config),
    orient = spec.orient;
  let enter, update;
  const encode = {
    enter: enter = {
      opacity: zero$1
    },
    update: update = {
      opacity: one
    },
    exit: {
      opacity: zero$1
    }
  };
  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$1);
  enter.x2 = update.x2 = ifX(orient, pos1);
  enter.y = update.y = ifY(orient, pos0, zero$1);
  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) {
  const _ = lookup(spec, config),
    orient = spec.orient,
    vscale = spec.gridScale,
    sign = getSign(orient, 1, -1),
    offset = offsetValue(spec.offset, sign);
  let enter, exit, update;
  const encode = {
    enter: enter = {
      opacity: zero$1
    },
    update: update = {
      opacity: one
    },
    exit: exit = {
      opacity: zero$1
    }
  };
  addEncoders(encode, {
    stroke: _('gridColor'),
    strokeCap: _('gridCap'),
    strokeDash: _('gridDash'),
    strokeDashOffset: _('gridDashOffset'),
    strokeOpacity: _('gridOpacity'),
    strokeWidth: _('gridWidth')
  });
  const tickPos = {
    scale: spec.scale,
    field: Value,
    band: band.band,
    extra: band.extra,
    offset: band.offset,
    round: _('tickRound')
  };
  const sz = ifX(orient, {
    signal: 'height'
  }, {
    signal: 'width'
  });
  const gridStart = vscale ? {
    scale: vscale,
    range: 0,
    mult: sign,
    offset: offset
  } : {
    value: 0,
    offset: offset
  };
  const gridEnd = vscale ? {
    scale: vscale,
    range: 1,
    mult: sign,
    offset: offset
  } : 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 (!isObject(offset)) {
    offset = isSignal(sign) ? {
      signal: `(${sign.signal}) * (${offset || 0})`
    } : sign * (offset || 0);
  } else {
    let entry = offset = extend({}, offset);
    while (entry.mult != null) {
      if (!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 = extend({}, entry.mult);
      }
    }
    entry.mult = sign;
  }
  return offset;
}

function axisTicks (spec, config, userEncode, dataRef, size, band) {
  const _ = lookup(spec, config),
    orient = spec.orient,
    sign = getSign(orient, -1, 1);
  let enter, exit, update;
  const encode = {
    enter: enter = {
      opacity: zero$1
    },
    update: update = {
      opacity: one
    },
    exit: exit = {
      opacity: zero$1
    }
  };
  addEncoders(encode, {
    stroke: _('tickColor'),
    strokeCap: _('tickCap'),
    strokeDash: _('tickDash'),
    strokeDashOffset: _('tickDashOffset'),
    strokeOpacity: _('tickOpacity'),
    strokeWidth: _('tickWidth')
  });
  const tickSize = encoder(size);
  tickSize.mult = sign;
  const tickPos = {
    scale: spec.scale,
    field: Value,
    band: band.band,
    extra: band.extra,
    offset: band.offset,
    round: _('tickRound')
  };
  update.y = enter.y = ifX(orient, zero$1, tickPos);
  update.y2 = enter.y2 = ifX(orient, tickSize);
  exit.x = ifX(orient, tickPos);
  update.x = enter.x = ifY(orient, zero$1, 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) {
  const _ = lookup(spec, config),
    orient = spec.orient,
    scale = spec.scale,
    sign = getSign(orient, -1, 1),
    flush = deref(_('labelFlush')),
    flushOffset = deref(_('labelFlushOffset')),
    labelAlign = _('labelAlign'),
    labelBaseline = _('labelBaseline');
  let flushOn = flush === 0 || !!flush,
    update;
  const tickSize = encoder(size);
  tickSize.mult = sign;
  tickSize.offset = encoder(_('labelPadding') || 0);
  tickSize.offset.mult = sign;
  const tickPos = {
    scale: scale,
    field: Value,
    band: 0.5,
    offset: extendOffset(band.offset, _('labelOffset'))
  };
  const align = ifX(orient, flushOn ? flushExpr(scale, flush, '"left"', '"right"', '"center"') : {
    value: 'center'
  }, ifRight(orient, 'left', 'right'));
  const baseline = ifX(orient, ifTop(orient, 'bottom', 'top'), flushOn ? flushExpr(scale, flush, '"top"', '"bottom"', '"middle"') : {
    value: 'middle'
  });
  const offsetExpr = flushExpr(scale, flush, `-(${flushOffset})`, flushOffset, 0);
  flushOn = flushOn && flushOffset;
  const enter = {
    opacity: zero$1,
    x: ifX(orient, tickPos, tickSize),
    y: ifY(orient, tickPos, tickSize)
  };
  const encode = {
    enter: enter,
    update: update = {
      opacity: one,
      text: {
        field: Label
      },
      x: enter.x,
      y: enter.y,
      align,
      baseline
    },
    exit: {
      opacity: zero$1,
      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
  });
  const bound = _('labelBound');
  let 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) {
  const _ = lookup(spec, config),
    orient = spec.orient,
    sign = getSign(orient, -1, 1);
  let enter, update;
  const encode = {
    enter: enter = {
      opacity: zero$1,
      anchor: encoder(_('titleAnchor', null)),
      align: {
        signal: alignExpr$1
      }
    },
    update: update = extend({}, enter, {
      opacity: one,
      text: encoder(spec.title)
    }),
    exit: {
      opacity: zero$1
    }
  };
  const 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$1, 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$1 (spec, scope) {
  const 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(_);

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

  // data source for axis ticks
  const 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
  const children = [];
  let size;

  // 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: extendEncode(buildAxisEncode(_, spec), axisEncode, Skip),
    marks: children,
    aria: _('aria'),
    description: _('description'),
    zindex: _('zindex'),
    name,
    interactive,
    style
  }), scope);
}
function buildAxisEncode(_, spec) {
  const 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) {
  const signals = array$2(spec.signals),
    scales = array$2(spec.scales);

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

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

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

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

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

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

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

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

  // parse legend definitions
  array$2(spec.legends).forEach(_ => parseLegend$1(_, 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
  array$2(spec.signals).forEach(s => {
    if (has$1(pre, s.name)) {
      // merge if built-in signal
      s = extend(pre[s.name], s);
    } else {
      // otherwise add to signal list
      signals.push(s);
    }
    map[s.name] = s;
  });

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

function Scope(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;
}
Scope.prototype = Subscope.prototype = {
  parse(spec) {
    return parseScope(spec, this);
  },
  fork() {
    return new Subscope(this);
  },
  isSubscope() {
    return this._subid > 0;
  },
  toRuntime() {
    this.finish();
    return {
      description: this.description,
      operators: this.operators,
      streams: this.streams,
      updates: this.updates,
      bindings: this.bindings,
      eventConfig: this.eventConfig,
      locale: this.locale
    };
  },
  id() {
    return (this._subid ? this._subid + ':' : 0) + this._id++;
  },
  add(op) {
    this.operators.push(op);
    op.id = this.id();
    // if pre-registration references exist, resolve them now
    if (op.refs) {
      op.refs.forEach(ref => {
        ref.$ref = op.id;
      });
      op.refs = null;
    }
    return op;
  },
  proxy(op) {
    const vref = op instanceof Entry ? ref(op) : op;
    return this.add(Proxy({
      value: vref
    }));
  },
  addStream(stream) {
    this.streams.push(stream);
    stream.id = this.id();
    return stream;
  },
  addUpdate(update) {
    this.updates.push(update);
    return update;
  },
  // Apply metadata
  finish() {
    let 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) {
      let 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 (const field in ds.index) {
        annotate(ds.index[field], name, 'index:' + field);
      }
    }
    return this;
  },
  // ----

  pushState(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);
  },
  popState() {
    this._encode.pop();
    this._parent.pop();
    this._lookup.pop();
    this._markpath.pop();
  },
  parent() {
    return peek$1(this._parent);
  },
  encode() {
    return peek$1(this._encode);
  },
  lookup() {
    return peek$1(this._lookup);
  },
  markpath() {
    const p = this._markpath;
    return ++p[p.length - 1];
  },
  // ----

  fieldRef(field, name) {
    if (isString(field)) return fieldRef$1(field, name);
    if (!field.signal) {
      error$1('Unsupported field reference: ' + $(field));
    }
    const s = field.signal;
    let f = this.field[s];
    if (!f) {
      const params = {
        name: this.signalRef(s)
      };
      if (name) params.as = name;
      this.field[s] = f = ref(this.add(Field(params)));
    }
    return f;
  },
  compareRef(cmp) {
    let signal = false;
    const check = _ => isSignal(_) ? (signal = true, this.signalRef(_.signal)) : isExpr$1(_) ? (signal = true, this.exprRef(_.expr)) : _;
    const fields = array$2(cmp.field).map(check),
      orders = array$2(cmp.order).map(check);
    return signal ? ref(this.add(Compare({
      fields: fields,
      orders: orders
    }))) : compareRef(fields, orders);
  },
  keyRef(fields, flat) {
    let signal = false;
    const check = _ => isSignal(_) ? (signal = true, ref(sig[_.signal])) : _;
    const sig = this.signals;
    fields = array$2(fields).map(check);
    return signal ? ref(this.add(Key({
      fields: fields,
      flat: flat
    }))) : keyRef(fields, flat);
  },
  sortRef(sort) {
    if (!sort) return sort;

    // including id ensures stable sorting
    const 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);
  },
  // ----

  event(source, type) {
    const key = source + ':' + type;
    if (!this.events[key]) {
      const id = this.id();
      this.streams.push({
        id: id,
        source: source,
        type: type
      });
      this.events[key] = id;
    }
    return this.events[key];
  },
  // ----

  hasOwnSignal(name) {
    return has$1(this.signals, name);
  },
  addSignal(name, value) {
    if (this.hasOwnSignal(name)) {
      error$1('Duplicate signal name: ' + $(name));
    }
    const op = value instanceof Entry ? value : this.add(operator(value));
    return this.signals[name] = op;
  },
  getSignal(name) {
    if (!this.signals[name]) {
      error$1('Unrecognized signal name: ' + $(name));
    }
    return this.signals[name];
  },
  signalRef(s) {
    if (this.signals[s]) {
      return ref(this.signals[s]);
    } else if (!has$1(this.lambdas, s)) {
      this.lambdas[s] = this.add(operator(null));
    }
    return ref(this.lambdas[s]);
  },
  parseLambdas() {
    const code = Object.keys(this.lambdas);
    for (let i = 0, n = code.length; i < n; ++i) {
      const s = code[i],
        e = parser(s, this),
        op = this.lambdas[s];
      op.params = e.$params;
      op.update = e.$expr;
    }
  },
  property(spec) {
    return spec && spec.signal ? this.signalRef(spec.signal) : spec;
  },
  objectProperty(spec) {
    return !spec || !isObject(spec) ? spec : this.signalRef(spec.signal || propertyLambda(spec));
  },
  exprRef(code, name) {
    const params = {
      expr: parser(code, this)
    };
    if (name) params.expr.$name = name;
    return ref(this.add(Expression(params)));
  },
  addBinding(name, bind) {
    if (!this.bindings) {
      error$1('Nested signals do not support binding: ' + $(name));
    }
    this.bindings.push(extend({
      signal: name
    }, bind));
  },
  // ----

  addScaleProj(name, transform) {
    if (has$1(this.scales, name)) {
      error$1('Duplicate scale or projection name: ' + $(name));
    }
    this.scales[name] = this.add(transform);
  },
  addScale(name, params) {
    this.addScaleProj(name, Scale(params));
  },
  addProjection(name, params) {
    this.addScaleProj(name, Projection(params));
  },
  getScale(name) {
    if (!this.scales[name]) {
      error$1('Unrecognized scale name: ' + $(name));
    }
    return this.scales[name];
  },
  scaleRef(name) {
    return ref(this.getScale(name));
  },
  scaleType(name) {
    return this.getScale(name).params.type;
  },
  projectionRef(name) {
    return this.scaleRef(name);
  },
  projectionType(name) {
    return this.scaleType(name);
  },
  // ----

  addData(name, dataScope) {
    if (has$1(this.data, name)) {
      error$1('Duplicate data set name: ' + $(name));
    }
    return this.data[name] = dataScope;
  },
  getData(name) {
    if (!this.data[name]) {
      error$1('Undefined data set name: ' + $(name));
    }
    return this.data[name];
  },
  addDataPipeline(name, entries) {
    if (has$1(this.data, name)) {
      error$1('Duplicate data set name: ' + $(name));
    }
    return this.addData(name, DataScope.fromEntries(this, entries));
  }
};
function propertyLambda(spec) {
  return (isArray(spec) ? arrayLambda : objectLambda)(spec);
}
function arrayLambda(array) {
  const n = array.length;
  let code = '[';
  for (let i = 0; i < n; ++i) {
    const value = array[i];
    code += (i > 0 ? ',' : '') + (isObject(value) ? value.signal || propertyLambda(value) : $(value));
  }
  return code + ']';
}
function objectLambda(obj) {
  let code = '{',
    i = 0,
    key,
    value;
  for (key in obj) {
    value = obj[key];
    code += (++i > 1 ? ',' : '') + $(key) + ':' + (isObject(value) ? value.signal || propertyLambda(value) : $(value));
  }
  return code + '}';
}

/**
 * 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
      },
      view: {
        fill: 'transparent'
      }
    },
    // 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 (spec, config, options) {
  if (!isObject(spec)) {
    error$1('Input Vega specification must be an object.');
  }
  config = mergeConfig(defaults(), config, spec.config);
  return parseView(spec, new Scope(config, options)).toRuntime();
}

var version$1$2 = "6.2.0";

// -- Transforms -----

extend(transforms, tx, vtx, encode$1, geo, force, label, tree, reg, voronoi, wordcloud, xf);
const version$3 = version$1$2;

const vegaImport = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  Bounds,
  CanvasHandler,
  CanvasRenderer,
  DATE,
  DAY,
  DAYOFYEAR,
  Dataflow,
  Debug,
  DisallowedObjectProperties,
  Error: Error$1,
  EventStream,
  Gradient: Gradient$1,
  GroupItem,
  HOURS,
  Handler: Handler$1,
  HybridHandler,
  HybridRenderer,
  Info,
  Item,
  MILLISECONDS,
  MINUTES,
  MONTH,
  Marks,
  MultiPulse,
  None: None$2,
  Operator,
  Parameters,
  Pulse,
  QUARTER,
  RenderType,
  Renderer,
  ResourceLoader,
  SECONDS,
  SVGHandler,
  SVGRenderer,
  SVGStringRenderer,
  Scenegraph,
  TIME_UNITS,
  Transform,
  View: View$1,
  WEEK,
  Warn,
  YEAR,
  accessor,
  accessorFields,
  accessorName,
  array: array$2,
  ascending: ascending$1,
  bandwidthNRD: estimateBandwidth,
  bin,
  bootstrapCI,
  boundClip,
  boundContext,
  boundItem: boundItem$1,
  boundMark,
  boundStroke,
  changeset,
  clampRange,
  codegenExpression: codegen,
  compare: compare$2,
  constant: constant$1,
  cumulativeLogNormal,
  cumulativeNormal,
  cumulativeUniform,
  dayofyear,
  debounce,
  defaultLocale,
  definition: definition$1,
  densityLogNormal,
  densityNormal,
  densityUniform,
  domChild,
  domClear,
  domCreate,
  domFind,
  dotbin,
  error: error$1,
  expressionFunction,
  extend,
  extent,
  extentIndex,
  falsy,
  fastmap,
  field: field$1,
  flush,
  font: font$1,
  fontFamily,
  fontSize,
  format: format$2,
  formatLocale: numberFormatDefaultLocale,
  formats: formats$1,
  hasOwnProperty: has$1,
  id,
  identity,
  inferType,
  inferTypes,
  ingest: ingest$1,
  inherits,
  inrange,
  interpolate: interpolate$1,
  interpolateColors,
  interpolateRange: interpolateRange$1,
  intersect: intersect$2,
  intersectBoxLine,
  intersectPath,
  intersectPoint,
  intersectRule,
  isArray,
  isBoolean: isBoolean$2,
  isDate: isDate$1,
  isFunction,
  isIterable,
  isNumber: isNumber$1,
  isObject,
  isRegExp,
  isString,
  isTuple,
  key,
  lerp,
  lineHeight,
  loader,
  locale,
  logger,
  lruCache,
  markup,
  merge: merge$2,
  mergeConfig,
  multiLineOffset,
  one: one$1,
  pad,
  panLinear,
  panLog,
  panPow,
  panSymlog,
  parse,
  parseExpression: parser$1,
  parseSelector: eventSelector,
  path: path$3,
  pathCurves: curves,
  pathEqual,
  pathParse: parse$3,
  pathRectangle: vg_rect,
  pathRender,
  pathSymbols: symbols$1,
  pathTrail: vg_trail,
  peek: peek$1,
  point: point$2,
  projection,
  quantileLogNormal,
  quantileNormal,
  quantileUniform,
  quantiles,
  quantizeInterpolator,
  quarter,
  quartiles,
  get random () { return random; },
  randomInteger: integer,
  randomKDE: kde,
  randomLCG: lcg,
  randomLogNormal: lognormal,
  randomMixture: mixture$1,
  randomNormal: gaussian,
  randomUniform: uniform,
  read,
  regressionConstant: constant,
  regressionExp: exp,
  regressionLinear: linear,
  regressionLoess: loess,
  regressionLog: log$1,
  regressionPoly: poly,
  regressionPow: pow,
  regressionQuad: quad,
  renderModule,
  repeat,
  resetDefaultLocale,
  resetSVGDefIds,
  responseType,
  runtimeContext: context,
  sampleCurve,
  sampleLogNormal,
  sampleNormal,
  sampleUniform,
  scale: scale$4,
  sceneEqual,
  sceneFromJSON,
  scenePickVisit: pickVisit,
  sceneToJSON,
  sceneVisit: visit,
  sceneZOrder: zorder,
  scheme: scheme$1,
  serializeXML,
  setHybridRendererOptions,
  setRandom,
  span,
  splitAccessPath,
  stringValue: $,
  textMetrics,
  timeBin: bin$1,
  timeFloor,
  timeFormatLocale: timeFormatDefaultLocale,
  timeInterval,
  timeOffset,
  timeSequence,
  timeUnitSpecifier,
  timeUnits,
  toBoolean,
  toDate,
  toNumber,
  toSet,
  toString,
  transform: transform$2,
  transforms,
  truncate: truncate$1,
  truthy,
  tupleid,
  typeParsers,
  utcFloor,
  utcInterval,
  utcOffset,
  utcSequence,
  utcdayofyear,
  utcquarter,
  utcweek,
  version: version$3,
  visitArray,
  week,
  writeConfig,
  zero: zero$2,
  zoomLinear,
  zoomLog,
  zoomPow,
  zoomSymlog
}, Symbol.toStringTag, { value: 'Module' }));

function adjustSpatial (item, encode, swap) {
  let t;
  if (encode.x2) {
    if (encode.x) {
      if (swap && item.x > item.x2) {
        t = item.x;
        item.x = item.x2;
        item.x2 = t;
      }
      item.width = item.x2 - item.x;
    } else {
      item.x = item.x2 - (item.width || 0);
    }
  }
  if (encode.xc) {
    item.x = item.xc - (item.width || 0) / 2;
  }
  if (encode.y2) {
    if (encode.y) {
      if (swap && item.y > item.y2) {
        t = item.y;
        item.y = item.y2;
        item.y2 = t;
      }
      item.height = item.y2 - item.y;
    } else {
      item.y = item.y2 - (item.height || 0);
    }
  }
  if (encode.yc) {
    item.y = item.yc - (item.height || 0) / 2;
  }
}

var Constants = {
  NaN: NaN,
  E: Math.E,
  LN2: Math.LN2,
  LN10: Math.LN10,
  LOG2E: Math.LOG2E,
  LOG10E: Math.LOG10E,
  PI: Math.PI,
  SQRT1_2: Math.SQRT1_2,
  SQRT2: Math.SQRT2,
  MIN_VALUE: Number.MIN_VALUE,
  MAX_VALUE: Number.MAX_VALUE
};

var Ops = {
  '*': (a, b) => a * b,
  '+': (a, b) => a + b,
  '-': (a, b) => a - b,
  '/': (a, b) => a / b,
  '%': (a, b) => a % b,
  '>': (a, b) => a > b,
  '<': (a, b) => a < b,
  '<=': (a, b) => a <= b,
  '>=': (a, b) => a >= b,
  '==': (a, b) => a == b,
  '!=': (a, b) => a != b,
  '===': (a, b) => a === b,
  '!==': (a, b) => a !== b,
  '&': (a, b) => a & b,
  '|': (a, b) => a | b,
  '^': (a, b) => a ^ b,
  '<<': (a, b) => a << b,
  '>>': (a, b) => a >> b,
  '>>>': (a, b) => a >>> b
};

var Unary = {
  '+': a => +a,
  '-': a => -a,
  '~': a => ~a,
  '!': a => !a
};

const slice = Array.prototype.slice;
const apply = (m, args, cast) => {
  const obj = cast ? cast(args[0]) : args[0];
  return obj[m].apply(obj, slice.call(args, 1));
};
const datetime = (yearOrTimestring, m = 0, d = 1, H = 0, M = 0, S = 0, ms = 0) => isString(yearOrTimestring) ? new Date(yearOrTimestring) : new Date(yearOrTimestring, m, d, H, M, S, ms);
var Functions = {
  // math functions
  isNaN: Number.isNaN,
  isFinite: Number.isFinite,
  abs: Math.abs,
  acos: Math.acos,
  asin: Math.asin,
  atan: Math.atan,
  atan2: Math.atan2,
  ceil: Math.ceil,
  cos: Math.cos,
  exp: Math.exp,
  floor: Math.floor,
  log: Math.log,
  max: Math.max,
  min: Math.min,
  pow: Math.pow,
  random: Math.random,
  round: Math.round,
  sin: Math.sin,
  sqrt: Math.sqrt,
  tan: Math.tan,
  clamp: (a, b, c) => Math.max(b, Math.min(c, a)),
  // date functions
  now: Date.now,
  utc: Date.UTC,
  datetime: datetime,
  date: d => new Date(d).getDate(),
  day: d => new Date(d).getDay(),
  year: d => new Date(d).getFullYear(),
  month: d => new Date(d).getMonth(),
  hours: d => new Date(d).getHours(),
  minutes: d => new Date(d).getMinutes(),
  seconds: d => new Date(d).getSeconds(),
  milliseconds: d => new Date(d).getMilliseconds(),
  time: d => new Date(d).getTime(),
  timezoneoffset: d => new Date(d).getTimezoneOffset(),
  utcdate: d => new Date(d).getUTCDate(),
  utcday: d => new Date(d).getUTCDay(),
  utcyear: d => new Date(d).getUTCFullYear(),
  utcmonth: d => new Date(d).getUTCMonth(),
  utchours: d => new Date(d).getUTCHours(),
  utcminutes: d => new Date(d).getUTCMinutes(),
  utcseconds: d => new Date(d).getUTCSeconds(),
  utcmilliseconds: d => new Date(d).getUTCMilliseconds(),
  // sequence functions
  length: x => x.length,
  join: function () {
    return apply('join', arguments);
  },
  indexof: function () {
    return apply('indexOf', arguments);
  },
  lastindexof: function () {
    return apply('lastIndexOf', arguments);
  },
  slice: function () {
    return apply('slice', arguments);
  },
  reverse: x => x.slice().reverse(),
  sort: x => x.slice().sort(ascending$1),
  // string functions
  parseFloat: parseFloat,
  parseInt: parseInt,
  upper: x => String(x).toUpperCase(),
  lower: x => String(x).toLowerCase(),
  substring: function () {
    return apply('substring', arguments, String);
  },
  split: function () {
    return apply('split', arguments, String);
  },
  replace: function () {
    return apply('replace', arguments, String);
  },
  trim: x => String(x).trim(),
  // Base64 encode/decode
  // Convert binary string to base64-encoded ascii
  btoa: x => btoa(x),
  // Convert base64-encoded ascii to binary string
  atob: x => atob(x),
  // regexp functions
  regexp: RegExp,
  test: (r, t) => RegExp(r).test(t)
};

const EventFunctions = ['view', 'item', 'group', 'xy', 'x', 'y'];
const DisallowedMethods = new Set([Function, eval, setTimeout, setInterval]);
if (typeof setImmediate === 'function') DisallowedMethods.add(setImmediate);
const Visitors = {
  Literal: ($, n) => n.value,
  Identifier: ($, n) => {
    const id = n.name;
    return $.memberDepth > 0 ? id : id === 'datum' ? $.datum : id === 'event' ? $.event : id === 'item' ? $.item : Constants[id] || $.params['$' + id];
  },
  MemberExpression: ($, n) => {
    const d = !n.computed,
      o = $(n.object);
    if (d) $.memberDepth += 1;
    const p = $(n.property);
    if (d) $.memberDepth -= 1;
    if (DisallowedMethods.has(o[p])) {
      // eslint-disable-next-line no-console
      console.error(`Prevented interpretation of member "${p}" which could lead to insecure code execution`);
      return;
    }
    return o[p];
  },
  CallExpression: ($, n) => {
    const args = n.arguments;
    let name = n.callee.name;

    // handle special internal functions used by encoders
    // re-route to corresponding standard function
    if (name.startsWith('_')) {
      name = name.slice(1);
    }

    // special case "if" due to conditional evaluation of branches
    return name === 'if' ? $(args[0]) ? $(args[1]) : $(args[2]) : ($.fn[name] || Functions[name]).apply($.fn, args.map($));
  },
  ArrayExpression: ($, n) => n.elements.map($),
  BinaryExpression: ($, n) => Ops[n.operator]($(n.left), $(n.right)),
  UnaryExpression: ($, n) => Unary[n.operator]($(n.argument)),
  ConditionalExpression: ($, n) => $(n.test) ? $(n.consequent) : $(n.alternate),
  LogicalExpression: ($, n) => n.operator === '&&' ? $(n.left) && $(n.right) : $(n.left) || $(n.right),
  ObjectExpression: ($, n) => n.properties.reduce((o, p) => {
    $.memberDepth += 1;
    const k = $(p.key);
    $.memberDepth -= 1;
    const v = $(p.value);
    if (DisallowedObjectProperties.has(k)) {
      // eslint-disable-next-line no-console
      console.error(`Prevented interpretation of property "${k}" which could lead to insecure code execution`);
    } else if (DisallowedMethods.has(v)) {
      // eslint-disable-next-line no-console
      console.error(`Prevented interpretation of method "${k}" which could lead to insecure code execution`);
    } else {
      o[k] = v;
    }
    return o;
  }, {})
};
function interpret (ast, fn, params, datum, event, item) {
  const $ = n => Visitors[n.type]($, n);
  $.memberDepth = 0;
  $.fn = Object.create(fn);
  $.params = params;
  $.datum = datum;
  $.event = event;
  $.item = item;

  // route event functions to annotated vega event context
  EventFunctions.forEach(f => $.fn[f] = (...args) => event.vega[f](...args));
  return $(ast);
}

var expression$1 = {
  /**
   * Parse an expression used to update an operator value.
   */
  operator(ctx, expr) {
    const ast = expr.ast,
      fn = ctx.functions;
    return _ => interpret(ast, fn, _);
  },
  /**
   * Parse an expression provided as an operator parameter value.
   */
  parameter(ctx, expr) {
    const ast = expr.ast,
      fn = ctx.functions;
    return (datum, _) => interpret(ast, fn, _, datum);
  },
  /**
   * Parse an expression applied to an event stream.
   */
  event(ctx, expr) {
    const ast = expr.ast,
      fn = ctx.functions;
    return event => interpret(ast, fn, undefined, undefined, event);
  },
  /**
   * Parse an expression used to handle an event-driven operator update.
   */
  handler(ctx, expr) {
    const ast = expr.ast,
      fn = ctx.functions;
    return (_, event) => {
      const datum = event.item && event.item.datum;
      return interpret(ast, fn, _, datum, event);
    };
  },
  /**
   * Parse an expression that performs visual encoding.
   */
  encode(ctx, encode) {
    const {
        marktype,
        channels
      } = encode,
      fn = ctx.functions,
      swap = marktype === 'group' || marktype === 'image' || marktype === 'rect';
    return (item, _) => {
      const datum = item.datum;
      let m = 0,
        v;
      for (const name in channels) {
        v = interpret(channels[name].ast, fn, _, datum, undefined, item);
        if (item[name] !== v) {
          item[name] = v;
          m = 1;
        }
      }
      if (marktype !== 'rule') {
        adjustSpatial(item, channels, swap);
      }
      return m;
    };
  }
};

var version$1$1 = "6.4.2";
var pkg$1 = {
	version: version$1$1};

function isLogicalOr(op) {
    return hasProperty(op, 'or');
}
function isLogicalAnd(op) {
    return hasProperty(op, 'and');
}
function isLogicalNot(op) {
    return hasProperty(op, 'not');
}
function forEachLeaf(op, fn) {
    if (isLogicalNot(op)) {
        forEachLeaf(op.not, fn);
    }
    else if (isLogicalAnd(op)) {
        for (const subop of op.and) {
            forEachLeaf(subop, fn);
        }
    }
    else if (isLogicalOr(op)) {
        for (const subop of op.or) {
            forEachLeaf(subop, fn);
        }
    }
    else {
        fn(op);
    }
}
function normalizeLogicalComposition(op, normalizer) {
    if (isLogicalNot(op)) {
        return { not: normalizeLogicalComposition(op.not, normalizer) };
    }
    else if (isLogicalAnd(op)) {
        return { and: op.and.map((o) => normalizeLogicalComposition(o, normalizer)) };
    }
    else if (isLogicalOr(op)) {
        return { or: op.or.map((o) => normalizeLogicalComposition(o, normalizer)) };
    }
    else {
        return normalizer(op);
    }
}

const duplicate = structuredClone;
function never(message) {
    throw new Error(message);
}
/**
 * Creates an object composed of the picked object properties.
 *
 * var object = {'a': 1, 'b': '2', 'c': 3};
 * pick(object, ['a', 'c']);
 * // → {'a': 1, 'c': 3}
 */
function pick(obj, props) {
    const copy = {};
    for (const prop of props) {
        if (has$1(obj, prop)) {
            copy[prop] = obj[prop];
        }
    }
    return copy;
}
/**
 * The opposite of _.pick; this method creates an object composed of the own
 * and inherited enumerable string keyed properties of object that are not omitted.
 */
function omit(obj, props) {
    const copy = { ...obj };
    for (const prop of props) {
        delete copy[prop];
    }
    return copy;
}
/**
 * Monkey patch Set so that `stringify` produces a string representation of sets.
 */
Set.prototype['toJSON'] = function () {
    return `Set(${[...this].map((x) => stringify$1(x)).join(',')})`;
};
/**
 * Converts any object to a string of limited size, or a number.
 */
function hash(a) {
    if (isNumber$1(a)) {
        return a;
    }
    const str = isString(a) ? a : stringify$1(a);
    // short strings can be used as hash directly, longer strings are hashed to reduce memory usage
    if (str.length < 250) {
        return str;
    }
    // from http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
    let h = 0;
    for (let i = 0; i < str.length; i++) {
        const char = str.charCodeAt(i);
        h = (h << 5) - h + char;
        h = h & h; // Convert to 32bit integer
    }
    return h;
}
function isNullOrFalse(x) {
    return x === false || x === null;
}
function contains(array, item) {
    return array.includes(item);
}
/**
 * Returns true if any item returns true.
 */
function some(arr, f) {
    let i = 0;
    for (const [k, a] of arr.entries()) {
        if (f(a, k, i++)) {
            return true;
        }
    }
    return false;
}
/**
 * Returns true if all items return true.
 */
function every(arr, f) {
    let i = 0;
    for (const [k, a] of arr.entries()) {
        if (!f(a, k, i++)) {
            return false;
        }
    }
    return true;
}
/**
 * recursively merges src into dest
 */
function mergeDeep$1(dest, ...src) {
    for (const s of src) {
        deepMerge_$1(dest, s ?? {});
    }
    return dest;
}
function deepMerge_$1(dest, src) {
    for (const property of keys(src)) {
        writeConfig(dest, property, src[property], true);
    }
}
function unique(values, f) {
    const results = [];
    const u = {};
    let v;
    for (const val of values) {
        v = f(val);
        if (v in u) {
            continue;
        }
        u[v] = 1;
        results.push(val);
    }
    return results;
}
/**
 * Returns true if the two dictionaries agree. Applies only to defined values.
 */
function isEqual(dict, other) {
    const dictKeys = keys(dict);
    const otherKeys = keys(other);
    if (dictKeys.length !== otherKeys.length) {
        return false;
    }
    for (const key of dictKeys) {
        if (dict[key] !== other[key]) {
            return false;
        }
    }
    return true;
}
function setEqual(a, b) {
    if (a.size !== b.size) {
        return false;
    }
    for (const e of a) {
        if (!b.has(e)) {
            return false;
        }
    }
    return true;
}
function hasIntersection(a, b) {
    for (const key of a) {
        if (b.has(key)) {
            return true;
        }
    }
    return false;
}
function prefixGenerator(a) {
    const prefixes = new Set();
    for (const x of a) {
        const splitField = splitAccessPath(x);
        // Wrap every element other than the first in `[]`
        const wrappedWithAccessors = splitField.map((y, i) => (i === 0 ? y : `[${y}]`));
        const computedPrefixes = wrappedWithAccessors.map((_, i) => wrappedWithAccessors.slice(0, i + 1).join(''));
        for (const y of computedPrefixes) {
            prefixes.add(y);
        }
    }
    return prefixes;
}
/**
 * Returns true if a and b have an intersection. Also return true if a or b are undefined
 * since this means we don't know what fields a node produces or depends on.
 */
function fieldIntersection(a, b) {
    if (a === undefined || b === undefined) {
        return true;
    }
    return hasIntersection(prefixGenerator(a), prefixGenerator(b));
}
function isEmpty(obj) {
    return keys(obj).length === 0;
}
// This is a stricter version of Object.keys but with better types. See https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208
const keys = Object.keys;
// Stricter version from https://github.com/microsoft/TypeScript/issues/51572#issuecomment-1319153323
const vals = Object.values;
// Stricter version from https://github.com/microsoft/TypeScript/issues/51572#issuecomment-1319153323
const entries$1 = Object.entries;
function isBoolean(b) {
    return b === true || b === false;
}
/**
 * Returns true if the value is a primitive type.
 */
function isPrimitive(v) {
    return isString(v) || isNumber$1(v) || isBoolean(v);
}
/**
 * Convert a string into a valid variable name
 */
function varName(s) {
    // Replace non-alphanumeric characters (anything besides a-zA-Z0-9_) with _
    const alphanumericS = s.replace(/\W/g, '_');
    // Add _ if the string has leading numbers.
    return (s.match(/^\d+/) ? '_' : '') + alphanumericS;
}
function logicalExpr(op, cb) {
    if (isLogicalNot(op)) {
        return `!(${logicalExpr(op.not, cb)})`;
    }
    else if (isLogicalAnd(op)) {
        return `(${op.and.map((and) => logicalExpr(and, cb)).join(') && (')})`;
    }
    else if (isLogicalOr(op)) {
        return `(${op.or.map((or) => logicalExpr(or, cb)).join(') || (')})`;
    }
    else {
        return cb(op);
    }
}
/**
 * Delete nested property of an object, and delete the ancestors of the property if they become empty.
 */
function deleteNestedProperty(obj, orderedProps) {
    if (orderedProps.length === 0) {
        return true;
    }
    const prop = orderedProps.shift();
    if (prop in obj && deleteNestedProperty(obj[prop], orderedProps)) {
        delete obj[prop];
    }
    return isEmpty(obj);
}
function titleCase(s) {
    return s.charAt(0).toUpperCase() + s.substr(1);
}
/**
 * Converts a path to an access path with datum.
 * @param path The field name.
 * @param datum The string to use for `datum`.
 */
function accessPathWithDatum(path, datum = 'datum') {
    const pieces = splitAccessPath(path);
    const prefixes = [];
    for (let i = 1; i <= pieces.length; i++) {
        const prefix = `[${pieces.slice(0, i).map($).join('][')}]`;
        prefixes.push(`${datum}${prefix}`);
    }
    return prefixes.join(' && ');
}
/**
 * Return access with datum to the flattened field.
 *
 * @param path The field name.
 * @param datum The string to use for `datum`.
 */
function flatAccessWithDatum(path, datum = 'datum') {
    return `${datum}[${$(splitAccessPath(path).join('.'))}]`;
}
/**
 * Return access with datum to **an unescaped path**.
 *
 * ```ts
 * console.log(accessWithDatumToUnescapedPath("vega's favorite"))
 * // "datum['vega\\'s favorite']"
 * ```
 *
 * @param path The unescaped path name. E.g., `"a.b"`, `"vega's favorite"`. (Note
 * that the field defs take escaped strings like `"a\\.b"`, `"vega\\'s favorite"`,
 * but this function is for the unescaped field/path)
 */
function accessWithDatumToUnescapedPath(unescapedPath) {
    const singleQuoteEscapedPath = unescapedPath.replaceAll("'", "\\'");
    return `datum['${singleQuoteEscapedPath}']`;
}
function unescapeSingleQuoteAndPathDot(escapedPath) {
    return escapedPath.replaceAll("\\'", "'").replaceAll('\\.', '.');
}
function escapePathAccess(string) {
    return string.replace(/(\[|\]|\.|'|")/g, '\\$1');
}
/**
 * Replaces path accesses with access to non-nested field.
 * For example, `foo["bar"].baz` becomes `foo\\.bar\\.baz`.
 */
function replacePathInField(path) {
    return `${splitAccessPath(path).map(escapePathAccess).join('\\.')}`;
}
/**
 * Replace all occurrences of a string with another string.
 *
 * @param string the string to replace in
 * @param find the string to replace
 * @param replacement the replacement
 */
function replaceAll(string, find, replacement) {
    return string.replace(new RegExp(find.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), replacement);
}
/**
 * Remove path accesses with access from field.
 * For example, `foo["bar"].baz` becomes `foo.bar.baz`.
 */
function removePathFromField(path) {
    return `${splitAccessPath(path).join('.')}`;
}
/**
 * Count the depth of the path. Returns 1 for fields that are not nested.
 */
function accessPathDepth(path) {
    if (!path) {
        return 0;
    }
    return splitAccessPath(path).length;
}
/**
 * This is a replacement for chained || for numeric properties or properties that respect null so that 0 will be included.
 */
function getFirstDefined(...args) {
    return args.find((a) => a !== undefined);
}
// variable used to generate id
let idCounter = 42;
/**
 * Returns a new random id every time it gets called.
 *
 * Has side effect!
 */
function uniqueId(prefix) {
    const id = ++idCounter;
    return prefix ? String(prefix) + id : id;
}
/**
 * Resets the id counter used in uniqueId. This can be useful for testing.
 */
function resetIdCounter() {
    idCounter = 42;
}
function internalField(name) {
    return isInternalField(name) ? name : `__${name}`;
}
function isInternalField(name) {
    return name.startsWith('__');
}
/**
 * Normalize angle to be within [0,360).
 */
function normalizeAngle(angle) {
    if (angle === undefined) {
        return undefined;
    }
    return ((angle % 360) + 360) % 360;
}
/**
 * Returns whether the passed in value is a valid number.
 */
function isNumeric(value) {
    if (isNumber$1(value)) {
        return true;
    }
    return !isNaN(value) && !isNaN(parseFloat(value));
}
const clonedProto = Object.getPrototypeOf(structuredClone({}));
/**
 * Compares two values for equality, including arrays and objects.
 *
 * Adapted from https://github.com/epoberezkin/fast-deep-equal.
 */
function deepEqual(a, b) {
    if (a === b)
        return true;
    if (a && b && typeof a == 'object' && typeof b == 'object') {
        // compare names to avoid issues with structured clone
        if (a.constructor.name !== b.constructor.name)
            return false;
        let length;
        let i;
        if (Array.isArray(a)) {
            length = a.length;
            if (length != b.length)
                return false;
            for (i = length; i-- !== 0;)
                if (!deepEqual(a[i], b[i]))
                    return false;
            return true;
        }
        if (a instanceof Map && b instanceof Map) {
            if (a.size !== b.size)
                return false;
            for (const e of a.entries())
                if (!b.has(e[0]))
                    return false;
            for (const e of a.entries())
                if (!deepEqual(e[1], b.get(e[0])))
                    return false;
            return true;
        }
        if (a instanceof Set && b instanceof Set) {
            if (a.size !== b.size)
                return false;
            for (const e of a.entries())
                if (!b.has(e[0]))
                    return false;
            return true;
        }
        if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
            length = a.length;
            if (length != b.length)
                return false;
            for (i = length; i-- !== 0;)
                if (a[i] !== b[i])
                    return false;
            return true;
        }
        if (a.constructor === RegExp)
            return a.source === b.source && a.flags === b.flags;
        // also compare to structured clone prototype
        if (a.valueOf !== Object.prototype.valueOf && a.valueOf !== clonedProto.valueOf)
            return a.valueOf() === b.valueOf();
        if (a.toString !== Object.prototype.toString && a.toString !== clonedProto.toString)
            return a.toString() === b.toString();
        const ks = Object.keys(a);
        length = ks.length;
        if (length !== Object.keys(b).length)
            return false;
        for (i = length; i-- !== 0;)
            if (!Object.prototype.hasOwnProperty.call(b, ks[i]))
                return false;
        for (i = length; i-- !== 0;) {
            const key = ks[i];
            if (!deepEqual(a[key], b[key]))
                return false;
        }
        return true;
    }
    // true if both NaN, false otherwise
    return a !== a && b !== b;
}
/**
 * Converts any object to a string representation that can be consumed by humans.
 *
 * Adapted from https://github.com/epoberezkin/fast-json-stable-stringify
 */
function stringify$1(data) {
    const seen = [];
    return (function _stringify(node) {
        if (node?.toJSON && typeof node.toJSON === 'function') {
            node = node.toJSON();
        }
        if (node === undefined)
            return undefined;
        if (typeof node == 'number')
            return isFinite(node) ? `${node}` : 'null';
        if (typeof node !== 'object')
            return JSON.stringify(node);
        let i;
        let out;
        if (Array.isArray(node)) {
            out = '[';
            for (i = 0; i < node.length; i++) {
                if (i)
                    out += ',';
                out += _stringify(node[i]) || 'null';
            }
            return `${out}]`;
        }
        if (node === null)
            return 'null';
        if (seen.includes(node)) {
            throw new TypeError('Converting circular structure to JSON');
        }
        const seenIndex = seen.push(node) - 1;
        const ks = Object.keys(node).sort();
        out = '';
        for (i = 0; i < ks.length; i++) {
            const key = ks[i];
            const value = _stringify(node[key]);
            if (!value)
                continue;
            if (out)
                out += ',';
            out += `${JSON.stringify(key)}:${value}`;
        }
        seen.splice(seenIndex, 1);
        return `{${out}}`;
    })(data);
}
/**
 * Check if the input object has the property and it's not undefined.
 *
 * @param object the object
 * @param property the property to search
 * @returns if the object has the property and it's not undefined.
 */
function hasProperty(obj, key) {
    return isObject(obj) && has$1(obj, key) && obj[key] !== undefined;
}

/*
 * Constants and utilities for encoding channels (Visual variables)
 * such as 'x', 'y', 'color'.
 */
// Facet
const ROW = 'row';
const COLUMN = 'column';
const FACET = 'facet';
// Position
const X = 'x';
const Y = 'y';
const X2 = 'x2';
const Y2 = 'y2';
// Position Offset
const XOFFSET = 'xOffset';
const YOFFSET = 'yOffset';
// Arc-Position
const RADIUS = 'radius';
const RADIUS2 = 'radius2';
const THETA = 'theta';
const THETA2 = 'theta2';
// Geo Position
const LATITUDE = 'latitude';
const LONGITUDE = 'longitude';
const LATITUDE2 = 'latitude2';
const LONGITUDE2 = 'longitude2';
// Time
const TIME = 'time';
// Mark property with scale
const COLOR = 'color';
const FILL = 'fill';
const STROKE = 'stroke';
const SHAPE = 'shape';
const SIZE = 'size';
const ANGLE = 'angle';
const OPACITY = 'opacity';
const FILLOPACITY = 'fillOpacity';
const STROKEOPACITY = 'strokeOpacity';
const STROKEWIDTH = 'strokeWidth';
const STROKEDASH = 'strokeDash';
// Non-scale channel
const TEXT$1 = 'text';
const ORDER = 'order';
const DETAIL = 'detail';
const KEY = 'key';
const TOOLTIP = 'tooltip';
const HREF = 'href';
const URL$1 = 'url';
const DESCRIPTION = 'description';
const POSITION_CHANNEL_INDEX = {
    x: 1,
    y: 1,
    x2: 1,
    y2: 1,
};
const POLAR_POSITION_CHANNEL_INDEX = {
    theta: 1,
    theta2: 1,
    radius: 1,
    radius2: 1,
};
function isPolarPositionChannel(c) {
    return has$1(POLAR_POSITION_CHANNEL_INDEX, c);
}
const GEO_POSIITON_CHANNEL_INDEX = {
    longitude: 1,
    longitude2: 1,
    latitude: 1,
    latitude2: 1,
};
function getPositionChannelFromLatLong(channel) {
    switch (channel) {
        case LATITUDE:
            return 'y';
        case LATITUDE2:
            return 'y2';
        case LONGITUDE:
            return 'x';
        case LONGITUDE2:
            return 'x2';
    }
}
function isGeoPositionChannel(c) {
    return has$1(GEO_POSIITON_CHANNEL_INDEX, c);
}
const GEOPOSITION_CHANNELS = keys(GEO_POSIITON_CHANNEL_INDEX);
const UNIT_CHANNEL_INDEX = {
    ...POSITION_CHANNEL_INDEX,
    ...POLAR_POSITION_CHANNEL_INDEX,
    ...GEO_POSIITON_CHANNEL_INDEX,
    xOffset: 1,
    yOffset: 1,
    // color
    color: 1,
    fill: 1,
    stroke: 1,
    // time
    time: 1,
    // other non-position with scale
    opacity: 1,
    fillOpacity: 1,
    strokeOpacity: 1,
    strokeWidth: 1,
    strokeDash: 1,
    size: 1,
    angle: 1,
    shape: 1,
    // channels without scales
    order: 1,
    text: 1,
    detail: 1,
    key: 1,
    tooltip: 1,
    href: 1,
    url: 1,
    description: 1,
};
function isColorChannel(channel) {
    return channel === COLOR || channel === FILL || channel === STROKE;
}
const FACET_CHANNEL_INDEX = {
    row: 1,
    column: 1,
    facet: 1,
};
const FACET_CHANNELS = keys(FACET_CHANNEL_INDEX);
const CHANNEL_INDEX = {
    ...UNIT_CHANNEL_INDEX,
    ...FACET_CHANNEL_INDEX,
};
const CHANNELS = keys(CHANNEL_INDEX);
const { order: _o, detail: _d, tooltip: _tt1, ...SINGLE_DEF_CHANNEL_INDEX } = CHANNEL_INDEX;
const { row: _r, column: _c, facet: _f, ...SINGLE_DEF_UNIT_CHANNEL_INDEX } = SINGLE_DEF_CHANNEL_INDEX;
function isSingleDefUnitChannel(str) {
    return has$1(SINGLE_DEF_UNIT_CHANNEL_INDEX, str);
}
function isChannel(str) {
    return has$1(CHANNEL_INDEX, str);
}
const SECONDARY_RANGE_CHANNEL = [X2, Y2, LATITUDE2, LONGITUDE2, THETA2, RADIUS2];
function isSecondaryRangeChannel(c) {
    const main = getMainRangeChannel(c);
    return main !== c;
}
/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
function getMainRangeChannel(channel) {
    switch (channel) {
        case X2:
            return X;
        case Y2:
            return Y;
        case LATITUDE2:
            return LATITUDE;
        case LONGITUDE2:
            return LONGITUDE;
        case THETA2:
            return THETA;
        case RADIUS2:
            return RADIUS;
    }
    return channel;
}
function getVgPositionChannel(channel) {
    if (isPolarPositionChannel(channel)) {
        switch (channel) {
            case THETA:
                return 'startAngle';
            case THETA2:
                return 'endAngle';
            case RADIUS:
                return 'outerRadius';
            case RADIUS2:
                return 'innerRadius';
        }
    }
    return channel;
}
/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
function getSecondaryRangeChannel(channel) {
    switch (channel) {
        case X:
            return X2;
        case Y:
            return Y2;
        case LATITUDE:
            return LATITUDE2;
        case LONGITUDE:
            return LONGITUDE2;
        case THETA:
            return THETA2;
        case RADIUS:
            return RADIUS2;
    }
    return undefined;
}
function getSizeChannel(channel) {
    switch (channel) {
        case X:
        case X2:
            return 'width';
        case Y:
        case Y2:
            return 'height';
    }
    return undefined;
}
/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
function getOffsetChannel(channel) {
    switch (channel) {
        case X:
            return 'xOffset';
        case Y:
            return 'yOffset';
        case X2:
            return 'x2Offset';
        case Y2:
            return 'y2Offset';
        case THETA:
            return 'thetaOffset';
        case RADIUS:
            return 'radiusOffset';
        case THETA2:
            return 'theta2Offset';
        case RADIUS2:
            return 'radius2Offset';
    }
    return undefined;
}
/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
function getOffsetScaleChannel(channel) {
    switch (channel) {
        case X:
            return 'xOffset';
        case Y:
            return 'yOffset';
    }
    return undefined;
}
function getMainChannelFromOffsetChannel(channel) {
    switch (channel) {
        case 'xOffset':
            return 'x';
        case 'yOffset':
            return 'y';
    }
}
// CHANNELS without COLUMN, ROW
const UNIT_CHANNELS = keys(UNIT_CHANNEL_INDEX);
// NONPOSITION_CHANNELS = UNIT_CHANNELS without X, Y, X2, Y2;
const { x: _x, y: _y, 
// x2 and y2 share the same scale as x and y
x2: _x2, y2: _y2, 
//
xOffset: _xo, yOffset: _yo, latitude: _latitude, longitude: _longitude, latitude2: _latitude2, longitude2: _longitude2, theta: _theta, theta2: _theta2, radius: _radius, radius2: _radius2, 
// The rest of unit channels then have scale
...NONPOSITION_CHANNEL_INDEX } = UNIT_CHANNEL_INDEX;
const NONPOSITION_CHANNELS = keys(NONPOSITION_CHANNEL_INDEX);
const POSITION_SCALE_CHANNEL_INDEX = {
    x: 1,
    y: 1,
};
const POSITION_SCALE_CHANNELS = keys(POSITION_SCALE_CHANNEL_INDEX);
function isXorY(channel) {
    return has$1(POSITION_SCALE_CHANNEL_INDEX, channel);
}
const POLAR_POSITION_SCALE_CHANNEL_INDEX = {
    theta: 1,
    radius: 1,
};
const POLAR_POSITION_SCALE_CHANNELS = keys(POLAR_POSITION_SCALE_CHANNEL_INDEX);
function getPositionScaleChannel(sizeType) {
    return sizeType === 'width' ? X : Y;
}
const OFFSET_SCALE_CHANNEL_INDEX = { xOffset: 1, yOffset: 1 };
function isXorYOffset(channel) {
    return has$1(OFFSET_SCALE_CHANNEL_INDEX, channel);
}
const TIME_SCALE_CHANNEL_INDEX = {
    time: 1,
};
function isTime(channel) {
    return channel in TIME_SCALE_CHANNEL_INDEX;
}
// NON_POSITION_SCALE_CHANNEL = SCALE_CHANNELS without position / offset
const { 
// x2 and y2 share the same scale as x and y
// text and tooltip have format instead of scale,
// href has neither format, nor scale
text: _t, tooltip: _tt, href: _hr, url: _u, description: _al, 
// detail and order have no scale
detail: _dd, key: _k, order: _oo, ...NONPOSITION_SCALE_CHANNEL_INDEX } = NONPOSITION_CHANNEL_INDEX;
const NONPOSITION_SCALE_CHANNELS = keys(NONPOSITION_SCALE_CHANNEL_INDEX);
function isNonPositionScaleChannel(channel) {
    return has$1(NONPOSITION_CHANNEL_INDEX, channel);
}
/**
 * @returns whether Vega supports legends for a particular channel
 */
function supportLegend(channel) {
    switch (channel) {
        case COLOR:
        case FILL:
        case STROKE:
        case SIZE:
        case SHAPE:
        case OPACITY:
        case STROKEWIDTH:
        case STROKEDASH:
            return true;
        case FILLOPACITY:
        case STROKEOPACITY:
        case ANGLE:
        case TIME:
            return false;
    }
}
// Declare SCALE_CHANNEL_INDEX
const SCALE_CHANNEL_INDEX = {
    ...POSITION_SCALE_CHANNEL_INDEX,
    ...POLAR_POSITION_SCALE_CHANNEL_INDEX,
    ...OFFSET_SCALE_CHANNEL_INDEX,
    ...NONPOSITION_SCALE_CHANNEL_INDEX,
};
/** List of channels with scales */
const SCALE_CHANNELS = keys(SCALE_CHANNEL_INDEX);
function isScaleChannel(channel) {
    return has$1(SCALE_CHANNEL_INDEX, channel);
}
/**
 * Return whether a channel supports a particular mark type.
 * @param channel  channel name
 * @param mark the mark type
 * @return whether the mark supports the channel
 */
function supportMark(channel, mark) {
    return getSupportedMark(channel)[mark];
}
const ALL_MARKS = {
    // all marks
    arc: 'always',
    area: 'always',
    bar: 'always',
    circle: 'always',
    geoshape: 'always',
    image: 'always',
    line: 'always',
    rule: 'always',
    point: 'always',
    rect: 'always',
    square: 'always',
    trail: 'always',
    text: 'always',
    tick: 'always',
};
const { geoshape: _g, ...ALL_MARKS_EXCEPT_GEOSHAPE } = ALL_MARKS;
/**
 * Return a dictionary showing whether a channel supports mark type.
 * @param channel
 * @return A dictionary mapping mark types to 'always', 'binned', or undefined
 */
function getSupportedMark(channel) {
    switch (channel) {
        case COLOR:
        case FILL:
        case STROKE:
        // falls through
        case DESCRIPTION:
        case DETAIL:
        case KEY:
        case TOOLTIP:
        case HREF:
        case ORDER: // TODO: revise (order might not support rect, which is not stackable?)
        case OPACITY:
        case FILLOPACITY:
        case STROKEOPACITY:
        case STROKEWIDTH:
        // falls through
        case FACET:
        case ROW: // falls through
        case COLUMN:
            return ALL_MARKS;
        case X:
        case Y:
        case XOFFSET:
        case YOFFSET:
        case LATITUDE:
        case LONGITUDE:
        case TIME:
            // all marks except geoshape. geoshape does not use X, Y -- it uses a projection
            return ALL_MARKS_EXCEPT_GEOSHAPE;
        case X2:
        case Y2:
        case LATITUDE2:
        case LONGITUDE2:
            return {
                area: 'always',
                bar: 'always',
                image: 'always',
                rect: 'always',
                rule: 'always',
                circle: 'binned',
                point: 'binned',
                square: 'binned',
                tick: 'binned',
                line: 'binned',
                trail: 'binned',
            };
        case SIZE:
            return {
                point: 'always',
                tick: 'always',
                rule: 'always',
                circle: 'always',
                square: 'always',
                bar: 'always',
                text: 'always',
                line: 'always',
                trail: 'always',
            };
        case STROKEDASH:
            return {
                line: 'always',
                point: 'always',
                tick: 'always',
                rule: 'always',
                circle: 'always',
                square: 'always',
                bar: 'always',
                geoshape: 'always',
            };
        case SHAPE:
            return { point: 'always', geoshape: 'always' };
        case TEXT$1:
            return { text: 'always' };
        case ANGLE:
            return { point: 'always', square: 'always', text: 'always' };
        case URL$1:
            return { image: 'always' };
        case THETA:
            return { text: 'always', arc: 'always' };
        case RADIUS:
            return { text: 'always', arc: 'always' };
        case THETA2:
        case RADIUS2:
            return { arc: 'always' };
    }
}
function rangeType(channel) {
    switch (channel) {
        case X:
        case Y:
        case THETA:
        case RADIUS:
        case XOFFSET:
        case YOFFSET:
        case SIZE:
        case ANGLE:
        case STROKEWIDTH:
        case OPACITY:
        case FILLOPACITY:
        case STROKEOPACITY:
        case TIME:
        // X2 and Y2 use X and Y scales, so they similarly have continuous range. [falls through]
        case X2:
        case Y2:
        case THETA2:
        case RADIUS2:
            return undefined;
        case FACET:
        case ROW:
        case COLUMN:
        case SHAPE:
        case STROKEDASH:
        // TEXT, TOOLTIP, URL, and HREF have no scale but have discrete output [falls through]
        case TEXT$1:
        case TOOLTIP:
        case HREF:
        case URL$1:
        case DESCRIPTION:
            return 'discrete';
        // Color can be either continuous or discrete, depending on scale type.
        case COLOR:
        case FILL:
        case STROKE:
            return 'flexible';
        // No scale, no range type.
        case LATITUDE:
        case LONGITUDE:
        case LATITUDE2:
        case LONGITUDE2:
        case DETAIL:
        case KEY:
        case ORDER:
            return undefined;
    }
}

const AGGREGATE_OP_INDEX = {
    argmax: 1,
    argmin: 1,
    average: 1,
    count: 1,
    distinct: 1,
    exponential: 1,
    exponentialb: 1,
    product: 1,
    max: 1,
    mean: 1,
    median: 1,
    min: 1,
    missing: 1,
    q1: 1,
    q3: 1,
    ci0: 1,
    ci1: 1,
    stderr: 1,
    stdev: 1,
    stdevp: 1,
    sum: 1,
    valid: 1,
    values: 1,
    variance: 1,
    variancep: 1,
};
const MULTIDOMAIN_SORT_OP_INDEX = {
    count: 1,
    min: 1,
    max: 1,
};
function isArgminDef(a) {
    return hasProperty(a, 'argmin');
}
function isArgmaxDef(a) {
    return hasProperty(a, 'argmax');
}
function isAggregateOp(a) {
    return isString(a) && has$1(AGGREGATE_OP_INDEX, a);
}
const COUNTING_OPS = new Set([
    'count',
    'valid',
    'missing',
    'distinct',
]);
function isCountingAggregateOp(aggregate) {
    return isString(aggregate) && COUNTING_OPS.has(aggregate);
}
function isMinMaxOp(aggregate) {
    return isString(aggregate) && contains(['min', 'max'], aggregate);
}
/** Additive-based aggregation operations. These can be applied to stack. */
const SUM_OPS = new Set([
    'count',
    'sum',
    'distinct',
    'valid',
    'missing',
]);
/**
 * Aggregation operators that always produce values within the range [domainMin, domainMax].
 */
const SHARED_DOMAIN_OPS = new Set([
    'mean',
    'average',
    'median',
    'q1',
    'q3',
    'min',
    'max',
]);

/**
 * Create a key for the bin configuration. Not for prebinned bin.
 */
function binToString(bin) {
    if (isBoolean$2(bin)) {
        bin = normalizeBin(bin, undefined);
    }
    return `bin${keys(bin)
        .map((p) => (isParameterExtent(bin[p]) ? varName(`_${p}_${entries$1(bin[p])}`) : varName(`_${p}_${bin[p]}`)))
        .join('')}`;
}
/**
 * Vega-Lite should bin the data.
 */
function isBinning(bin) {
    return bin === true || (isBinParams(bin) && !bin.binned);
}
/**
 * The data is already binned and so Vega-Lite should not bin it again.
 */
function isBinned(bin) {
    return bin === 'binned' || (isBinParams(bin) && bin.binned === true);
}
function isBinParams(bin) {
    return isObject(bin);
}
function isParameterExtent(extent) {
    return hasProperty(extent, 'param');
}
function autoMaxBins(channel) {
    switch (channel) {
        case ROW:
        case COLUMN:
        case SIZE:
        case COLOR:
        case FILL:
        case STROKE:
        case STROKEWIDTH:
        case OPACITY:
        case FILLOPACITY:
        case STROKEOPACITY:
        // Facets and Size shouldn't have too many bins
        // We choose 6 like shape to simplify the rule [falls through]
        case SHAPE:
            return 6; // Vega's "shape" has 6 distinct values
        case STROKEDASH:
            return 4; // We only provide 5 different stroke dash values (but 4 is more effective)
        default:
            return 10;
    }
}

function isExprRef(o) {
    return hasProperty(o, 'expr');
}
function replaceExprRef(index, { level } = { level: 0 }) {
    const props = keys(index || {});
    const newIndex = {};
    for (const prop of props) {
        newIndex[prop] = level === 0 ? signalRefOrValue(index[prop]) : replaceExprRef(index[prop], { level: level - 1 });
    }
    return newIndex;
}

function extractTitleConfig(titleConfig) {
    const { 
    // These are non-mark title config that need to be hardcoded
    anchor, frame, offset, orient, angle, limit, 
    // color needs to be redirect to fill
    color, 
    // subtitle properties
    subtitleColor, subtitleFont, subtitleFontSize, subtitleFontStyle, subtitleFontWeight, subtitleLineHeight, subtitlePadding, 
    // The rest are mark config.
    ...rest } = titleConfig;
    const titleMarkConfig = {
        ...rest,
        ...(color ? { fill: color } : {}),
    };
    // These are non-mark title config that need to be hardcoded
    const nonMarkTitleProperties = {
        ...(anchor ? { anchor } : {}),
        ...(frame ? { frame } : {}),
        ...(offset ? { offset } : {}),
        ...(orient ? { orient } : {}),
        ...(angle !== undefined ? { angle } : {}),
        ...(limit !== undefined ? { limit } : {}),
    };
    // subtitle part can stay in config.title since header titles do not use subtitle
    const subtitle = {
        ...(subtitleColor ? { subtitleColor } : {}),
        ...(subtitleFont ? { subtitleFont } : {}),
        ...(subtitleFontSize ? { subtitleFontSize } : {}),
        ...(subtitleFontStyle ? { subtitleFontStyle } : {}),
        ...(subtitleFontWeight ? { subtitleFontWeight } : {}),
        ...(subtitleLineHeight ? { subtitleLineHeight } : {}),
        ...(subtitlePadding ? { subtitlePadding } : {}),
    };
    const subtitleMarkConfig = pick(titleConfig, ['align', 'baseline', 'dx', 'dy', 'limit']);
    return { titleMarkConfig, subtitleMarkConfig, nonMarkTitleProperties, subtitle };
}
function isText(v) {
    return isString(v) || (isArray(v) && isString(v[0]));
}

function isSignalRef(o) {
    return hasProperty(o, 'signal');
}
function isVgRangeStep(range) {
    return hasProperty(range, 'step');
}
function isDataRefUnionedDomain(domain) {
    if (!isArray(domain)) {
        return hasProperty(domain, 'fields') && !hasProperty(domain, 'data');
    }
    return false;
}
function isFieldRefUnionDomain(domain) {
    if (!isArray(domain)) {
        return hasProperty(domain, 'fields') && hasProperty(domain, 'data');
    }
    return false;
}
function isDataRefDomain(domain) {
    if (!isArray(domain)) {
        return hasProperty(domain, 'field') && hasProperty(domain, 'data');
    }
    return false;
}
const VG_MARK_CONFIG_INDEX = {
    aria: 1,
    description: 1,
    ariaRole: 1,
    ariaRoleDescription: 1,
    blend: 1,
    opacity: 1,
    fill: 1,
    fillOpacity: 1,
    stroke: 1,
    strokeCap: 1,
    strokeWidth: 1,
    strokeOpacity: 1,
    strokeDash: 1,
    strokeDashOffset: 1,
    strokeJoin: 1,
    strokeOffset: 1,
    strokeMiterLimit: 1,
    startAngle: 1,
    endAngle: 1,
    padAngle: 1,
    innerRadius: 1,
    outerRadius: 1,
    size: 1,
    shape: 1,
    interpolate: 1,
    tension: 1,
    orient: 1,
    align: 1,
    baseline: 1,
    text: 1,
    dir: 1,
    dx: 1,
    dy: 1,
    ellipsis: 1,
    limit: 1,
    radius: 1,
    theta: 1,
    angle: 1,
    font: 1,
    fontSize: 1,
    fontWeight: 1,
    fontStyle: 1,
    lineBreak: 1,
    lineHeight: 1,
    cursor: 1,
    href: 1,
    tooltip: 1,
    cornerRadius: 1,
    cornerRadiusTopLeft: 1,
    cornerRadiusTopRight: 1,
    cornerRadiusBottomLeft: 1,
    cornerRadiusBottomRight: 1,
    aspect: 1,
    width: 1,
    height: 1,
    url: 1,
    smooth: 1,
    // commented below are vg channel that do not have mark config.
    // x: 1,
    // y: 1,
    // x2: 1,
    // y2: 1,
    // xc'|'yc'
    // clip: 1,
    // path: 1,
    // url: 1,
};
const VG_MARK_CONFIGS = keys(VG_MARK_CONFIG_INDEX);
const VG_MARK_INDEX = {
    arc: 1,
    area: 1,
    group: 1,
    image: 1,
    line: 1,
    path: 1,
    rect: 1,
    rule: 1,
    shape: 1,
    symbol: 1,
    text: 1,
    trail: 1,
};
// Vega's cornerRadius channels.
const VG_CORNERRADIUS_CHANNELS = [
    'cornerRadius',
    'cornerRadiusTopLeft',
    'cornerRadiusTopRight',
    'cornerRadiusBottomLeft',
    'cornerRadiusBottomRight',
];

const BIN_RANGE_DELIMITER = ' \u2013 ';
function signalOrValueRefWithCondition(val) {
    const condition = isArray(val.condition)
        ? val.condition.map(conditionalSignalRefOrValue)
        : conditionalSignalRefOrValue(val.condition);
    return {
        ...signalRefOrValue(val),
        condition,
    };
}
function signalRefOrValue(value) {
    if (isExprRef(value)) {
        const { expr, ...rest } = value;
        return { signal: expr, ...rest };
    }
    return value;
}
function conditionalSignalRefOrValue(value) {
    if (isExprRef(value)) {
        const { expr, ...rest } = value;
        return { signal: expr, ...rest };
    }
    return value;
}
function signalOrValueRef(value) {
    if (isExprRef(value)) {
        const { expr, ...rest } = value;
        return { signal: expr, ...rest };
    }
    if (isSignalRef(value)) {
        return value;
    }
    return value !== undefined ? { value } : undefined;
}
function exprFromSignalRefOrValue(ref) {
    if (isSignalRef(ref)) {
        return ref.signal;
    }
    return $(ref);
}
function exprFromValueRefOrSignalRef(ref) {
    if (isSignalRef(ref)) {
        return ref.signal;
    }
    return $(ref.value);
}
function signalOrStringValue(v) {
    if (isSignalRef(v)) {
        return v.signal;
    }
    return v == null ? null : $(v);
}
function applyMarkConfig(e, model, propsList) {
    for (const property of propsList) {
        const value = getMarkConfig(property, model.markDef, model.config);
        if (value !== undefined) {
            e[property] = signalOrValueRef(value);
        }
    }
    return e;
}
function getStyles(mark) {
    return [].concat(mark.type, mark.style ?? []);
}
function getMarkPropOrConfig(channel, mark, config, opt = {}) {
    const { vgChannel, ignoreVgConfig } = opt;
    if (vgChannel && hasProperty(mark, vgChannel)) {
        return mark[vgChannel];
    }
    else if (mark[channel] !== undefined) {
        return mark[channel];
    }
    else if (ignoreVgConfig && (!vgChannel || vgChannel === channel)) {
        return undefined;
    }
    return getMarkConfig(channel, mark, config, opt);
}
/**
 * Return property value from style or mark specific config property if exists.
 * Otherwise, return general mark specific config.
 */
function getMarkConfig(channel, mark, config, { vgChannel } = {}) {
    const cfg = getMarkStyleConfig(channel, mark, config.style);
    return getFirstDefined(
    // style config has highest precedence
    vgChannel ? cfg : undefined, cfg, 
    // then mark-specific config
    vgChannel ? config[mark.type][vgChannel] : undefined, config[mark.type][channel], // Need to cast because MarkDef doesn't perfectly match with AnyMarkConfig, but if the type isn't available, we'll get nothing here, which is fine
    // If there is vgChannel, skip vl channel.
    // For example, vl size for text is vg fontSize, but config.mark.size is only for point size.
    vgChannel ? config.mark[vgChannel] : config.mark[channel]);
}
function getMarkStyleConfig(prop, mark, styleConfigIndex) {
    return getStyleConfig(prop, getStyles(mark), styleConfigIndex);
}
function getStyleConfig(p, styles, styleConfigIndex) {
    styles = array$2(styles);
    let value;
    for (const style of styles) {
        const styleConfig = styleConfigIndex[style];
        if (hasProperty(styleConfig, p)) {
            value = styleConfig[p];
        }
    }
    return value;
}
/**
 * Return Vega sort parameters (tuple of field and order).
 */
function sortParams(orderDef, fieldRefOption) {
    return array$2(orderDef).reduce((s, orderChannelDef) => {
        s.field.push(vgField(orderChannelDef, fieldRefOption));
        s.order.push(orderChannelDef.sort ?? 'ascending');
        return s;
    }, { field: [], order: [] });
}
function mergeTitleFieldDefs(f1, f2) {
    const merged = [...f1];
    f2.forEach((fdToMerge) => {
        for (const fieldDef1 of merged) {
            // If already exists, no need to append to merged array
            if (deepEqual(fieldDef1, fdToMerge)) {
                return;
            }
        }
        merged.push(fdToMerge);
    });
    return merged;
}
function mergeTitle(title1, title2) {
    if (deepEqual(title1, title2) || !title2) {
        // if titles are the same or title2 is falsy
        return title1;
    }
    else if (!title1) {
        // if title1 is falsy
        return title2;
    }
    else {
        return [...array$2(title1), ...array$2(title2)].join(', ');
    }
}
function mergeTitleComponent(v1, v2) {
    const v1Val = v1.value;
    const v2Val = v2.value;
    if (v1Val == null || v2Val === null) {
        return {
            explicit: v1.explicit,
            value: null,
        };
    }
    else if ((isText(v1Val) || isSignalRef(v1Val)) && (isText(v2Val) || isSignalRef(v2Val))) {
        return {
            explicit: v1.explicit,
            value: mergeTitle(v1Val, v2Val),
        };
    }
    else if (isText(v1Val) || isSignalRef(v1Val)) {
        return {
            explicit: v1.explicit,
            value: v1Val,
        };
    }
    else if (isText(v2Val) || isSignalRef(v2Val)) {
        return {
            explicit: v1.explicit,
            value: v2Val,
        };
    }
    else if (!isText(v1Val) && !isSignalRef(v1Val) && !isText(v2Val) && !isSignalRef(v2Val)) {
        return {
            explicit: v1.explicit,
            value: mergeTitleFieldDefs(v1Val, v2Val),
        };
    }
    /* istanbul ignore next: Condition should not happen -- only for warning in development. */
    throw new Error('It should never reach here');
}

/**
 * Collection of all Vega-Lite Error Messages
 */
function invalidSpec(spec) {
    return `Invalid specification ${stringify$1(spec)}. Make sure the specification includes at least one of the following properties: "mark", "layer", "facet", "hconcat", "vconcat", "concat", or "repeat".`;
}
// FIT
const FIT_NON_SINGLE = 'Autosize "fit" only works for single views and layered views.';
function containerSizeNonSingle(name) {
    const uName = name == 'width' ? 'Width' : 'Height';
    return `${uName} "container" only works for single views and layered views.`;
}
function containerSizeNotCompatibleWithAutosize(name) {
    const uName = name == 'width' ? 'Width' : 'Height';
    const fitDirection = name == 'width' ? 'x' : 'y';
    return `${uName} "container" only works well with autosize "fit" or "fit-${fitDirection}".`;
}
function droppingFit(channel) {
    return channel
        ? `Dropping "fit-${channel}" because spec has discrete ${getSizeChannel(channel)}.`
        : `Dropping "fit" because spec has discrete size.`;
}
// VIEW SIZE
function unknownField(channel) {
    return `Unknown field for ${channel}. Cannot calculate view size.`;
}
// SELECTION
function cannotProjectOnChannelWithoutField(channel) {
    return `Cannot project a selection on encoding channel "${channel}", which has no field.`;
}
function cannotProjectAggregate(channel, aggregate) {
    return `Cannot project a selection on encoding channel "${channel}" as it uses an aggregate function ("${aggregate}").`;
}
function nearestNotSupportForContinuous(mark) {
    return `The "nearest" transform is not supported for ${mark} marks.`;
}
function selectionNotSupported(mark) {
    return `Selection not supported for ${mark} yet.`;
}
function selectionNotFound(name) {
    return `Cannot find a selection named "${name}".`;
}
const SCALE_BINDINGS_CONTINUOUS = 'Scale bindings are currently only supported for scales with unbinned, continuous domains.';
const SEQUENTIAL_SCALE_DEPRECATED = 'Sequntial scales are deprecated. The available quantitative scale type values are linear, log, pow, sqrt, symlog, time and utc';
const LEGEND_BINDINGS_MUST_HAVE_PROJECTION = 'Legend bindings are only supported for selections over an individual field or encoding channel.';
function cannotLookupVariableParameter(name) {
    return `Lookups can only be performed on selection parameters. "${name}" is a variable parameter.`;
}
function noSameUnitLookup(name) {
    return (`Cannot define and lookup the "${name}" selection in the same view. ` +
        `Try moving the lookup into a second, layered view?`);
}
const NEEDS_SAME_SELECTION = 'The same selection must be used to override scale domains in a layered view.';
const INTERVAL_INITIALIZED_WITH_POS = 'Interval selections should be initialized using "x", "y", "longitude", or "latitude" keys.';
// REPEAT
function noSuchRepeatedValue(field) {
    return `Unknown repeated value "${field}".`;
}
function columnsNotSupportByRowCol(type) {
    return `The "columns" property cannot be used when "${type}" has nested row/column.`;
}
const MULTIPLE_TIMER_ANIMATION_SELECTION = 'Multiple timer selections in one unit spec are not supported. Ignoring all but the first.';
const MULTI_VIEW_ANIMATION_UNSUPPORTED = 'Animation involving facet, layer, or concat is currently unsupported.';
function selectionAsScaleDomainWithoutField(field) {
    return ('A "field" or "encoding" must be specified when using a selection as a scale domain. ' +
        `Using "field": ${$(field)}.`);
}
function selectionAsScaleDomainWrongEncodings(encodings, encoding, extent, field) {
    return (`${!encodings.length ? 'No ' : 'Multiple '}matching ${$(encoding)} encoding found for selection ${$(extent.param)}. ` +
        `Using "field": ${$(field)}.`);
}
// CONCAT / REPEAT
const CONCAT_CANNOT_SHARE_AXIS = 'Axes cannot be shared in concatenated or repeated views yet (https://github.com/vega/vega-lite/issues/2415).';
// DATA
function unrecognizedParse(p) {
    return `Unrecognized parse "${p}".`;
}
function differentParse(field, local, ancestor) {
    return `An ancestor parsed field "${field}" as ${ancestor} but a child wants to parse the field as ${local}.`;
}
const ADD_SAME_CHILD_TWICE = 'Attempt to add the same child twice.';
// TRANSFORMS
function invalidTransformIgnored(transform) {
    return `Ignoring an invalid transform: ${stringify$1(transform)}.`;
}
const NO_FIELDS_NEEDS_AS = 'If "from.fields" is not specified, "as" has to be a string that specifies the key to be used for the data from the secondary source.';
// ENCODING & FACET
function customFormatTypeNotAllowed(channel) {
    return `Config.customFormatTypes is not true, thus custom format type and format for channel ${channel} are dropped.`;
}
function projectionOverridden(opt) {
    const { parentProjection, projection } = opt;
    return `Layer's shared projection ${stringify$1(parentProjection)} is overridden by a child projection ${stringify$1(projection)}.`;
}
const REPLACE_ANGLE_WITH_THETA = 'Arc marks uses theta channel rather than angle, replacing angle with theta.';
function offsetNestedInsideContinuousPositionScaleDropped(mainChannel) {
    return `${mainChannel}Offset dropped because ${mainChannel} is continuous`;
}
function primitiveChannelDef(channel, type, value) {
    return `Channel ${channel} is a ${type}. Converted to {value: ${stringify$1(value)}}.`;
}
function invalidFieldType(type) {
    return `Invalid field type "${type}".`;
}
function invalidFieldTypeForCountAggregate(type, aggregate) {
    return `Invalid field type "${type}" for aggregate: "${aggregate}", using "quantitative" instead.`;
}
function invalidAggregate(aggregate) {
    return `Invalid aggregation operator "${aggregate}".`;
}
function droppingColor(type, opt) {
    const { fill, stroke } = opt;
    return `Dropping color ${type} as the plot also has ${fill && stroke ? 'fill and stroke' : fill ? 'fill' : 'stroke'}.`;
}
function relativeBandSizeNotSupported(sizeChannel) {
    return `Position range does not support relative band size for ${sizeChannel}.`;
}
function emptyFieldDef(fieldDef, channel) {
    return `Dropping ${stringify$1(fieldDef)} from channel "${channel}" since it does not contain any data field, datum, value, or signal.`;
}
const LINE_WITH_VARYING_SIZE = 'Line marks cannot encode size with a non-groupby field. You may want to use trail marks instead.';
function incompatibleChannel(channel, markOrFacet, when) {
    return `${channel} dropped as it is incompatible with "${markOrFacet}"${''}.`;
}
function invalidEncodingChannel(channel) {
    return `${channel}-encoding is dropped as ${channel} is not a valid encoding channel.`;
}
function channelShouldBeDiscrete(channel) {
    return `${channel} encoding should be discrete (ordinal / nominal / binned).`;
}
function channelShouldBeDiscreteOrDiscretizing(channel) {
    return `${channel} encoding should be discrete (ordinal / nominal / binned) or use a discretizing scale (e.g. threshold).`;
}
function facetChannelDropped(channels) {
    return `Facet encoding dropped as ${channels.join(' and ')} ${channels.length > 1 ? 'are' : 'is'} also specified.`;
}
function discreteChannelCannotEncode(channel, type) {
    return `Using discrete channel "${channel}" to encode "${type}" field can be misleading as it does not encode ${type === 'ordinal' ? 'order' : 'magnitude'}.`;
}
// MARK
function rangeMarkAlignmentCannotBeExpression(align) {
    return `The ${align} for range marks cannot be an expression`;
}
function lineWithRange(hasX2, hasY2) {
    const channels = hasX2 && hasY2 ? 'x2 and y2' : hasX2 ? 'x2' : 'y2';
    return `Line mark is for continuous lines and thus cannot be used with ${channels}. We will use the rule mark (line segments) instead.`;
}
function orientOverridden(original, actual) {
    return `Specified orient "${original}" overridden with "${actual}".`;
}
function cannotUseScalePropertyWithNonColor(prop) {
    return `Cannot use the scale property "${prop}" with non-color channel.`;
}
function cannotUseRelativeBandSizeWithNonBandScale(scaleType) {
    return `Cannot use the relative band size with ${scaleType} scale.`;
}
function unaggregateDomainHasNoEffectForRawField(fieldDef) {
    return `Using unaggregated domain with raw field has no effect (${stringify$1(fieldDef)}).`;
}
function unaggregateDomainWithNonSharedDomainOp(aggregate) {
    return `Unaggregated domain not applicable for "${aggregate}" since it produces values outside the origin domain of the source data.`;
}
function unaggregatedDomainWithLogScale(fieldDef) {
    return `Unaggregated domain is currently unsupported for log scale (${stringify$1(fieldDef)}).`;
}
function cannotApplySizeToNonOrientedMark(mark) {
    return `Cannot apply size to non-oriented mark "${mark}".`;
}
function scaleTypeNotWorkWithChannel(channel, scaleType, defaultScaleType) {
    return `Channel "${channel}" does not work with "${scaleType}" scale. We are using "${defaultScaleType}" scale instead.`;
}
function scaleTypeNotWorkWithFieldDef(scaleType, defaultScaleType) {
    return `FieldDef does not work with "${scaleType}" scale. We are using "${defaultScaleType}" scale instead.`;
}
function scalePropertyNotWorkWithScaleType(scaleType, propName, channel) {
    return `${channel}-scale's "${propName}" is dropped as it does not work with ${scaleType} scale.`;
}
function stepDropped(channel) {
    return `The step for "${channel}" is dropped because the ${channel === 'width' ? 'x' : 'y'} is continuous.`;
}
function mergeConflictingProperty(property, propertyOf, v1, v2) {
    return `Conflicting ${propertyOf.toString()} property "${property.toString()}" (${stringify$1(v1)} and ${stringify$1(v2)}). Using ${stringify$1(v1)}.`;
}
function mergeConflictingDomainProperty(property, propertyOf, v1, v2) {
    return `Conflicting ${propertyOf.toString()} property "${property.toString()}" (${stringify$1(v1)} and ${stringify$1(v2)}). Using the union of the two domains.`;
}
function independentScaleMeansIndependentGuide(channel) {
    return `Setting the scale to be independent for "${channel}" means we also have to set the guide (axis or legend) to be independent.`;
}
function domainSortDropped(sort) {
    return `Dropping sort property ${stringify$1(sort)} as unioned domains only support boolean or op "count", "min", and "max".`;
}
const MORE_THAN_ONE_SORT = 'Domains that should be unioned has conflicting sort properties. Sort will be set to true.';
const FACETED_INDEPENDENT_DIFFERENT_SOURCES = 'Detected faceted independent scales that union domain of multiple fields from different data sources. We will use the first field. The result view size may be incorrect.';
const FACETED_INDEPENDENT_SAME_FIELDS_DIFFERENT_SOURCES = 'Detected faceted independent scales that union domain of the same fields from different source. We will assume that this is the same field from a different fork of the same data source. However, if this is not the case, the result view size may be incorrect.';
const FACETED_INDEPENDENT_SAME_SOURCE = 'Detected faceted independent scales that union domain of multiple fields from the same data source. We will use the first field. The result view size may be incorrect.';
// LEGEND
function legendValuesUnioned(channelA, channelB) {
    return `Unioning discrete legend values from ${channelA} and ${channelB}.`;
}
// STACK
function cannotStackRangedMark(channel) {
    return `Cannot stack "${channel}" if there is already "${channel}2".`;
}
function stackNonLinearScale(scaleType) {
    return `Stack is applied to a non-linear scale (${scaleType}).`;
}
function stackNonSummativeAggregate(aggregate) {
    return `Stacking is applied even though the aggregate function is non-summative ("${aggregate}").`;
}
// TIMEUNIT
function invalidTimeUnit(unitName, value) {
    return `Invalid ${unitName}: ${stringify$1(value)}.`;
}
function droppedDay(d) {
    return `Dropping day from datetime ${stringify$1(d)} as day cannot be combined with other units.`;
}
function errorBarCenterAndExtentAreNotNeeded(center, extent) {
    return `${extent ? 'extent ' : ''}${extent && center ? 'and ' : ''}${center ? 'center ' : ''}${extent && center ? 'are ' : 'is '}not needed when data are aggregated.`;
}
function errorBarCenterIsUsedWithWrongExtent(center, extent, mark) {
    return `${center} is not usually used with ${extent} for ${mark}.`;
}
function errorBarContinuousAxisHasCustomizedAggregate(aggregate, compositeMark) {
    return `Continuous axis should not have customized aggregation function ${aggregate}; ${compositeMark} already agregates the axis.`;
}
function errorBand1DNotSupport(property) {
    return `1D error band does not support ${property}.`;
}
// CHANNEL
function channelRequiredForBinned(channel) {
    return `Channel ${channel} is required for "binned" bin.`;
}
function channelShouldNotBeUsedForBinned(channel) {
    return `Channel ${channel} should not be used with "binned" bin.`;
}
function domainRequiredForThresholdScale(channel) {
    return `Domain for ${channel} is required for threshold scale.`;
}

/**
 * Vega-Lite's singleton logger utility.
 */
/**
 * Main (default) Vega Logger instance for Vega-Lite.
 */
const main = logger(Warn);
let current = main;
/**
 * Set the singleton logger to be a custom logger.
 */
function set(newLogger) {
    current = newLogger;
    return current;
}
/**
 * Reset the main logger to use the default Vega Logger.
 */
function reset() {
    current = main;
    return current;
}
function error(...args) {
    current.error(...args);
}
function warn(...args) {
    current.warn(...args);
}
function debug(...args) {
    current.debug(...args);
}

// DateTime definition object
function isDateTime(o) {
    if (o && isObject(o)) {
        for (const part of TIMEUNIT_PARTS) {
            if (hasProperty(o, part)) {
                return true;
            }
        }
    }
    return false;
}
const MONTHS = [
    'january',
    'february',
    'march',
    'april',
    'may',
    'june',
    'july',
    'august',
    'september',
    'october',
    'november',
    'december',
];
const SHORT_MONTHS = MONTHS.map((m) => m.substr(0, 3));
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
const SHORT_DAYS = DAYS.map((d) => d.substr(0, 3));
function normalizeQuarter(q) {
    if (isNumeric(q)) {
        q = +q;
    }
    if (isNumber$1(q)) {
        if (q > 4) {
            warn(invalidTimeUnit('quarter', q));
        }
        // We accept 1-based quarter, so need to readjust to 0-based quarter
        return q - 1;
    }
    else {
        // Invalid quarter
        throw new Error(invalidTimeUnit('quarter', q));
    }
}
function normalizeMonth(m) {
    if (isNumeric(m)) {
        m = +m;
    }
    if (isNumber$1(m)) {
        // We accept 1-based month, so need to readjust to 0-based month
        return m - 1;
    }
    else {
        const lowerM = m.toLowerCase();
        const monthIndex = MONTHS.indexOf(lowerM);
        if (monthIndex !== -1) {
            return monthIndex; // 0 for january, ...
        }
        const shortM = lowerM.substr(0, 3);
        const shortMonthIndex = SHORT_MONTHS.indexOf(shortM);
        if (shortMonthIndex !== -1) {
            return shortMonthIndex;
        }
        // Invalid month
        throw new Error(invalidTimeUnit('month', m));
    }
}
function normalizeDay(d) {
    if (isNumeric(d)) {
        d = +d;
    }
    if (isNumber$1(d)) {
        // mod so that this can be both 0-based where 0 = sunday
        // and 1-based where 7=sunday
        return d % 7;
    }
    else {
        const lowerD = d.toLowerCase();
        const dayIndex = DAYS.indexOf(lowerD);
        if (dayIndex !== -1) {
            return dayIndex; // 0 for january, ...
        }
        const shortD = lowerD.substr(0, 3);
        const shortDayIndex = SHORT_DAYS.indexOf(shortD);
        if (shortDayIndex !== -1) {
            return shortDayIndex;
        }
        // Invalid day
        throw new Error(invalidTimeUnit('day', d));
    }
}
/**
 * @param d the date.
 * @param normalize whether to normalize quarter, month, day. This should probably be true if d is a DateTime.
 * @returns array of date time parts [year, month, day, hours, minutes, seconds, milliseconds]
 */
function dateTimeParts(d, normalize) {
    const parts = [];
    if (normalize && d.day !== undefined) {
        if (keys(d).length > 1) {
            warn(droppedDay(d));
            d = duplicate(d);
            delete d.day;
        }
    }
    if (d.year !== undefined) {
        parts.push(d.year);
    }
    else {
        // Just like Vega's timeunit transform, set default year to 2012, so domain conversion will be compatible with Vega
        // Note: 2012 is a leap year (and so the date February 29 is respected) that begins on a Sunday (and so days of the week will order properly at the beginning of the year).
        parts.push(2012);
    }
    if (d.month !== undefined) {
        const month = normalize ? normalizeMonth(d.month) : d.month;
        parts.push(month);
    }
    else if (d.quarter !== undefined) {
        const quarter = normalize ? normalizeQuarter(d.quarter) : d.quarter;
        parts.push(isNumber$1(quarter) ? quarter * 3 : `${quarter}*3`);
    }
    else {
        parts.push(0); // months start at zero in JS
    }
    if (d.date !== undefined) {
        parts.push(d.date);
    }
    else if (d.day !== undefined) {
        // HACK: Day only works as a standalone unit
        // This is only correct because we always set year to 2006 for day
        const day = normalize ? normalizeDay(d.day) : d.day;
        parts.push(isNumber$1(day) ? day + 1 : `${day}+1`);
    }
    else {
        parts.push(1); // Date starts at 1 in JS
    }
    // Note: can't use TimeUnit enum here as importing it will create
    // circular dependency problem!
    for (const timeUnit of ['hours', 'minutes', 'seconds', 'milliseconds']) {
        const unit = d[timeUnit];
        parts.push(typeof unit === 'undefined' ? 0 : unit);
    }
    return parts;
}
/**
 * Return Vega expression for a date time.
 *
 * @param d the date time.
 * @returns the Vega expression.
 */
function dateTimeToExpr(d) {
    const parts = dateTimeParts(d, true);
    const string = parts.join(', ');
    if (d.utc) {
        return `utc(${string})`;
    }
    else {
        return `datetime(${string})`;
    }
}
/**
 * Return Vega expression for a date time expression.
 *
 * @param d the internal date time object with expression.
 * @returns the Vega expression.
 */
function dateTimeExprToExpr(d) {
    const parts = dateTimeParts(d, false);
    const string = parts.join(', ');
    if (d.utc) {
        return `utc(${string})`;
    }
    else {
        return `datetime(${string})`;
    }
}
/**
 * @param d the date time.
 * @returns the timestamp.
 */
function dateTimeToTimestamp(d) {
    const parts = dateTimeParts(d, true);
    if (d.utc) {
        return +new Date(Date.UTC(...parts));
    }
    else {
        return +new Date(...parts);
    }
}

/** Time Unit that only corresponds to only one part of Date objects. */
const LOCAL_SINGLE_TIMEUNIT_INDEX = {
    year: 1,
    quarter: 1,
    month: 1,
    week: 1,
    day: 1,
    dayofyear: 1,
    date: 1,
    hours: 1,
    minutes: 1,
    seconds: 1,
    milliseconds: 1,
};
const TIMEUNIT_PARTS = keys(LOCAL_SINGLE_TIMEUNIT_INDEX);
function isLocalSingleTimeUnit(timeUnit) {
    return has$1(LOCAL_SINGLE_TIMEUNIT_INDEX, timeUnit);
}
function isBinnedTimeUnit(timeUnit) {
    if (isObject(timeUnit)) {
        return timeUnit.binned;
    }
    return isBinnedTimeUnitString(timeUnit);
}
function isBinnedTimeUnitString(timeUnit) {
    return timeUnit?.startsWith('binned');
}
function isUTCTimeUnit(t) {
    return t.startsWith('utc');
}
function getLocalTimeUnitFromUTCTimeUnit(t) {
    return t.substring(3);
}
// In order of increasing specificity
const VEGALITE_TIMEFORMAT = {
    'year-month': '%b %Y ',
    'year-month-date': '%b %d, %Y ',
};
function getTimeUnitParts(timeUnit) {
    return TIMEUNIT_PARTS.filter((part) => containsTimeUnit(timeUnit, part));
}
function getSmallestTimeUnitPart(timeUnit) {
    const parts = getTimeUnitParts(timeUnit);
    return parts[parts.length - 1];
}
/** Returns true if fullTimeUnit contains the timeUnit, false otherwise. */
function containsTimeUnit(fullTimeUnit, timeUnit) {
    const index = fullTimeUnit.indexOf(timeUnit);
    if (index < 0) {
        return false;
    }
    // exclude milliseconds
    if (index > 0 && timeUnit === 'seconds' && fullTimeUnit.charAt(index - 1) === 'i') {
        return false;
    }
    // exclude dayofyear
    if (fullTimeUnit.length > index + 3 && timeUnit === 'day' && fullTimeUnit.charAt(index + 3) === 'o') {
        return false;
    }
    if (index > 0 && timeUnit === 'year' && fullTimeUnit.charAt(index - 1) === 'f') {
        return false;
    }
    return true;
}
/**
 * Returns Vega expression for a given timeUnit and fieldRef
 */
function fieldExpr(fullTimeUnit, field, { end } = { end: false }) {
    const fieldRef = accessPathWithDatum(field);
    const utc = isUTCTimeUnit(fullTimeUnit) ? 'utc' : '';
    function func(timeUnit) {
        if (timeUnit === 'quarter') {
            // quarter starting at 0 (0,3,6,9).
            return `(${utc}quarter(${fieldRef})-1)`;
        }
        else {
            return `${utc}${timeUnit}(${fieldRef})`;
        }
    }
    let lastTimeUnit;
    const dateExpr = {};
    for (const part of TIMEUNIT_PARTS) {
        if (containsTimeUnit(fullTimeUnit, part)) {
            dateExpr[part] = func(part);
            lastTimeUnit = part;
        }
    }
    if (end) {
        dateExpr[lastTimeUnit] += '+1';
    }
    return dateTimeExprToExpr(dateExpr);
}
function timeUnitSpecifierExpression(timeUnit) {
    if (!timeUnit) {
        return undefined;
    }
    const timeUnitParts = getTimeUnitParts(timeUnit);
    return `timeUnitSpecifier(${stringify$1(timeUnitParts)}, ${stringify$1(VEGALITE_TIMEFORMAT)})`;
}
/**
 * Returns the signal expression used for axis labels for a time unit.
 */
function formatExpression(timeUnit, field, isUTCScale) {
    if (!timeUnit) {
        return undefined;
    }
    const expr = timeUnitSpecifierExpression(timeUnit);
    // We only use utcFormat for utc scale
    // For utc time units, the data is already converted as a part of timeUnit transform.
    // Thus, utc time units should use timeFormat to avoid shifting the time twice.
    const utc = isUTCScale || isUTCTimeUnit(timeUnit);
    return `${utc ? 'utc' : 'time'}Format(${field}, ${expr})`;
}
function normalizeTimeUnit(timeUnit) {
    if (!timeUnit) {
        return undefined;
    }
    let params;
    if (isString(timeUnit)) {
        if (isBinnedTimeUnitString(timeUnit)) {
            params = {
                unit: timeUnit.substring(6),
                binned: true,
            };
        }
        else {
            params = {
                unit: timeUnit,
            };
        }
    }
    else if (isObject(timeUnit)) {
        params = {
            ...timeUnit,
            ...(timeUnit.unit ? { unit: timeUnit.unit } : {}),
        };
    }
    if (isUTCTimeUnit(params.unit)) {
        params.utc = true;
        params.unit = getLocalTimeUnitFromUTCTimeUnit(params.unit);
    }
    return params;
}
function timeUnitToString(tu) {
    const { utc, ...rest } = normalizeTimeUnit(tu);
    if (rest.unit) {
        return ((utc ? 'utc' : '') +
            keys(rest)
                .map((p) => varName(`${p === 'unit' ? '' : `_${p}_`}${rest[p]}`))
                .join(''));
    }
    else {
        // when maxbins is specified instead of units
        return `${utc ? 'utc' : ''}timeunit${keys(rest)
            .map((p) => varName(`_${p}_${rest[p]}`))
            .join('')}`;
    }
}
function durationExpr(timeUnit, wrap = (x) => x) {
    const normalizedTimeUnit = normalizeTimeUnit(timeUnit);
    const smallestUnitPart = getSmallestTimeUnitPart(normalizedTimeUnit.unit);
    if (smallestUnitPart && smallestUnitPart !== 'day') {
        const startDate = {
            year: 2001, // pick a non-leap year
            month: 1,
            date: 1,
            hours: 0,
            minutes: 0,
            seconds: 0,
            milliseconds: 0,
        };
        const { step, part } = getDateTimePartAndStep(smallestUnitPart, normalizedTimeUnit.step);
        const endDate = {
            ...startDate,
            [part]: +startDate[part] + step,
        };
        // Calculate timestamp duration for the smallest unit listed
        return `${wrap(dateTimeToExpr(endDate))} - ${wrap(dateTimeToExpr(startDate))}`;
    }
    return undefined;
}
const DATE_PARTS = {
    year: 1,
    month: 1,
    date: 1,
    hours: 1,
    minutes: 1,
    seconds: 1,
    milliseconds: 1,
};
function isDatePart(timeUnit) {
    return has$1(DATE_PARTS, timeUnit);
}
function getDateTimePartAndStep(timeUnit, step = 1) {
    if (isDatePart(timeUnit)) {
        return { part: timeUnit, step };
    }
    switch (timeUnit) {
        case 'day':
        case 'dayofyear':
            return { part: 'date', step };
        case 'quarter':
            return { part: 'month', step: step * 3 };
        case 'week':
            return { part: 'date', step: step * 7 };
    }
}

function isSelectionPredicate(predicate) {
    return hasProperty(predicate, 'param');
}
function isFieldEqualPredicate(predicate) {
    return !!predicate?.field && predicate.equal !== undefined;
}
function isFieldLTPredicate(predicate) {
    return !!predicate?.field && predicate.lt !== undefined;
}
function isFieldLTEPredicate(predicate) {
    return !!predicate?.field && predicate.lte !== undefined;
}
function isFieldGTPredicate(predicate) {
    return !!predicate?.field && predicate.gt !== undefined;
}
function isFieldGTEPredicate(predicate) {
    return !!predicate?.field && predicate.gte !== undefined;
}
function isFieldRangePredicate(predicate) {
    if (predicate?.field) {
        if (isArray(predicate.range) && predicate.range.length === 2) {
            return true;
        }
        else if (isSignalRef(predicate.range)) {
            return true;
        }
    }
    return false;
}
function isFieldOneOfPredicate(predicate) {
    return (!!predicate?.field && (isArray(predicate.oneOf) || isArray(predicate.in)) // backward compatibility
    );
}
function isFieldValidPredicate(predicate) {
    return !!predicate?.field && predicate.valid !== undefined;
}
function isFieldPredicate(predicate) {
    return (isFieldOneOfPredicate(predicate) ||
        isFieldEqualPredicate(predicate) ||
        isFieldRangePredicate(predicate) ||
        isFieldLTPredicate(predicate) ||
        isFieldGTPredicate(predicate) ||
        isFieldLTEPredicate(predicate) ||
        isFieldGTEPredicate(predicate));
}
function predicateValueExpr(v, timeUnit) {
    return valueExpr(v, { timeUnit, wrapTime: true });
}
function predicateValuesExpr(vals, timeUnit) {
    return vals.map((v) => predicateValueExpr(v, timeUnit));
}
// This method is used by Voyager. Do not change its behavior without changing Voyager.
function fieldFilterExpression(predicate, useInRange = true) {
    const { field } = predicate;
    const normalizedTimeUnit = normalizeTimeUnit(predicate.timeUnit);
    const { unit, binned } = normalizedTimeUnit || {};
    const rawFieldExpr = vgField(predicate, { expr: 'datum' });
    const fieldExpr$1 = unit
        ? // For timeUnit, cast into integer with time() so we can use ===, inrange, indexOf to compare values directly.
            // TODO: We calculate timeUnit on the fly here. Consider if we would like to consolidate this with timeUnit pipeline
            // TODO: support utc
            `time(${!binned ? fieldExpr(unit, field) : rawFieldExpr})`
        : rawFieldExpr;
    if (isFieldEqualPredicate(predicate)) {
        return `${fieldExpr$1}===${predicateValueExpr(predicate.equal, unit)}`;
    }
    else if (isFieldLTPredicate(predicate)) {
        const upper = predicate.lt;
        return `${fieldExpr$1}<${predicateValueExpr(upper, unit)}`;
    }
    else if (isFieldGTPredicate(predicate)) {
        const lower = predicate.gt;
        return `${fieldExpr$1}>${predicateValueExpr(lower, unit)}`;
    }
    else if (isFieldLTEPredicate(predicate)) {
        const upper = predicate.lte;
        return `${fieldExpr$1}<=${predicateValueExpr(upper, unit)}`;
    }
    else if (isFieldGTEPredicate(predicate)) {
        const lower = predicate.gte;
        return `${fieldExpr$1}>=${predicateValueExpr(lower, unit)}`;
    }
    else if (isFieldOneOfPredicate(predicate)) {
        return `indexof([${predicateValuesExpr(predicate.oneOf, unit).join(',')}], ${fieldExpr$1}) !== -1`;
    }
    else if (isFieldValidPredicate(predicate)) {
        return fieldValidPredicate(fieldExpr$1, predicate.valid);
    }
    else if (isFieldRangePredicate(predicate)) {
        const { range } = replaceExprRef(predicate);
        const lower = isSignalRef(range) ? { signal: `${range.signal}[0]` } : range[0];
        const upper = isSignalRef(range) ? { signal: `${range.signal}[1]` } : range[1];
        if (lower !== null && upper !== null && useInRange) {
            return `inrange(${fieldExpr$1}, [${predicateValueExpr(lower, unit)}, ${predicateValueExpr(upper, unit)}])`;
        }
        const exprs = [];
        if (lower !== null) {
            exprs.push(`${fieldExpr$1} >= ${predicateValueExpr(lower, unit)}`);
        }
        if (upper !== null) {
            exprs.push(`${fieldExpr$1} <= ${predicateValueExpr(upper, unit)}`);
        }
        return exprs.length > 0 ? exprs.join(' && ') : 'true';
    }
    /* istanbul ignore next: it should never reach here */
    throw new Error(`Invalid field predicate: ${stringify$1(predicate)}`);
}
function fieldValidPredicate(fieldExpr, valid = true) {
    if (valid) {
        return `isValid(${fieldExpr}) && isFinite(+${fieldExpr})`;
    }
    else {
        return `!isValid(${fieldExpr}) || !isFinite(+${fieldExpr})`;
    }
}
function normalizePredicate$1(f) {
    if (isFieldPredicate(f) && f.timeUnit) {
        return {
            ...f,
            timeUnit: normalizeTimeUnit(f.timeUnit),
        };
    }
    return f;
}

/**
 * Data type based on level of measurement
 */
const Type = {
    quantitative: 'quantitative',
    ordinal: 'ordinal',
    temporal: 'temporal',
    nominal: 'nominal',
    geojson: 'geojson',
};
function isContinuous(type) {
    return type === 'quantitative' || type === 'temporal';
}
function isDiscrete$1(type) {
    return type === 'ordinal' || type === 'nominal';
}
const QUANTITATIVE = Type.quantitative;
const ORDINAL = Type.ordinal;
const TEMPORAL = Type.temporal;
const NOMINAL = Type.nominal;
const GEOJSON = Type.geojson;
/**
 * Get full, lowercase type name for a given type.
 * @param  type
 * @return Full type name.
 */
function getFullName(type) {
    if (type) {
        type = type.toLowerCase();
        switch (type) {
            case 'q':
            case QUANTITATIVE:
                return 'quantitative';
            case 't':
            case TEMPORAL:
                return 'temporal';
            case 'o':
            case ORDINAL:
                return 'ordinal';
            case 'n':
            case NOMINAL:
                return 'nominal';
            case GEOJSON:
                return 'geojson';
        }
    }
    // If we get invalid input, return undefined type.
    return undefined;
}

const ScaleType = {
    // Continuous - Quantitative
    LINEAR: 'linear',
    LOG: 'log',
    POW: 'pow',
    SQRT: 'sqrt',
    // Continuous - Time
    TIME: 'time',
    UTC: 'utc',
    POINT: 'point',
    BAND: 'band',
};
/**
 * Index for scale categories -- only scale of the same categories can be merged together.
 * Current implementation is trying to be conservative and avoid merging scale type that might not work together
 */
const SCALE_CATEGORY_INDEX = {
    linear: 'numeric',
    log: 'numeric',
    pow: 'numeric',
    sqrt: 'numeric',
    symlog: 'numeric',
    identity: 'numeric',
    sequential: 'numeric',
    time: 'time',
    utc: 'time',
    ordinal: 'ordinal',
    'bin-ordinal': 'bin-ordinal', // TODO: should bin-ordinal support merging with other
    point: 'ordinal-position',
    band: 'ordinal-position',
    quantile: 'discretizing',
    quantize: 'discretizing',
    threshold: 'discretizing',
};
/**
 * Whether the two given scale types can be merged together.
 */
function scaleCompatible(scaleType1, scaleType2) {
    const scaleCategory1 = SCALE_CATEGORY_INDEX[scaleType1];
    const scaleCategory2 = SCALE_CATEGORY_INDEX[scaleType2];
    return (scaleCategory1 === scaleCategory2 ||
        (scaleCategory1 === 'ordinal-position' && scaleCategory2 === 'time') ||
        (scaleCategory2 === 'ordinal-position' && scaleCategory1 === 'time'));
}
/**
 * Index for scale precedence -- high score = higher priority for merging.
 */
const SCALE_PRECEDENCE_INDEX = {
    // numeric
    linear: 0,
    log: 1,
    pow: 1,
    sqrt: 1,
    symlog: 1,
    identity: 1,
    sequential: 1,
    // time
    time: 0,
    utc: 0,
    // ordinal-position -- these have higher precedence than continuous scales as they support more types of data
    point: 10,
    band: 11, // band has higher precedence as it is better for interaction
    // non grouped types
    ordinal: 0,
    'bin-ordinal': 0,
    quantile: 0,
    quantize: 0,
    threshold: 0,
};
/**
 * Return scale categories -- only scale of the same categories can be merged together.
 */
function scaleTypePrecedence(scaleType) {
    return SCALE_PRECEDENCE_INDEX[scaleType];
}
const QUANTITATIVE_SCALES = new Set([
    'linear',
    'log',
    'pow',
    'sqrt',
    'symlog',
]);
const CONTINUOUS_TO_CONTINUOUS_SCALES = new Set([
    ...QUANTITATIVE_SCALES,
    'time',
    'utc',
]);
function isQuantitative(type) {
    return QUANTITATIVE_SCALES.has(type);
}
const CONTINUOUS_TO_DISCRETE_SCALES = new Set([
    'quantile',
    'quantize',
    'threshold',
]);
const CONTINUOUS_DOMAIN_SCALES = new Set([
    ...CONTINUOUS_TO_CONTINUOUS_SCALES,
    ...CONTINUOUS_TO_DISCRETE_SCALES,
    'sequential',
    'identity',
]);
const DISCRETE_DOMAIN_SCALES = new Set([
    'ordinal',
    'bin-ordinal',
    'point',
    'band',
]);
function hasDiscreteDomain(type) {
    return DISCRETE_DOMAIN_SCALES.has(type);
}
function hasContinuousDomain(type) {
    return CONTINUOUS_DOMAIN_SCALES.has(type);
}
function isContinuousToContinuous(type) {
    return CONTINUOUS_TO_CONTINUOUS_SCALES.has(type);
}
function isContinuousToDiscrete(type) {
    return CONTINUOUS_TO_DISCRETE_SCALES.has(type);
}
const defaultScaleConfig = {
    pointPadding: 0.5,
    barBandPaddingInner: 0.1,
    rectBandPaddingInner: 0,
    tickBandPaddingInner: 0.25,
    bandWithNestedOffsetPaddingInner: 0.2,
    bandWithNestedOffsetPaddingOuter: 0.2,
    minBandSize: 2,
    minFontSize: 8,
    maxFontSize: 40,
    minOpacity: 0.3,
    maxOpacity: 0.8,
    // FIXME: revise if these *can* become ratios of width/height step
    minSize: 4, // Point size is area. For square point, 9 = 3 pixel ^ 2, not too small!
    minStrokeWidth: 1,
    maxStrokeWidth: 4,
    quantileCount: 4,
    quantizeCount: 4,
    zero: true,
    framesPerSecond: 2,
    animationDuration: 5,
};
function isExtendedScheme(scheme) {
    return !isString(scheme) && hasProperty(scheme, 'name');
}
function isParameterDomain(domain) {
    return hasProperty(domain, 'param');
}
function isDomainUnionWith(domain) {
    return hasProperty(domain, 'unionWith');
}
function isFieldRange(range) {
    return isObject(range) && 'field' in range;
}
const SCALE_PROPERTY_INDEX = {
    type: 1,
    domain: 1,
    domainMax: 1,
    domainMin: 1,
    domainMid: 1,
    domainRaw: 1,
    align: 1,
    range: 1,
    rangeMax: 1,
    rangeMin: 1,
    scheme: 1,
    bins: 1,
    // Other properties
    reverse: 1,
    round: 1,
    // quantitative / time
    clamp: 1,
    nice: 1,
    // quantitative
    base: 1,
    exponent: 1,
    constant: 1,
    interpolate: 1,
    zero: 1, // zero depends on domain
    // band/point
    padding: 1,
    paddingInner: 1,
    paddingOuter: 1,
};
const { type, domain: domain$1, range: range$1, rangeMax, rangeMin, scheme, ...NON_TYPE_DOMAIN_RANGE_VEGA_SCALE_PROPERTY_INDEX } = SCALE_PROPERTY_INDEX;
const NON_TYPE_DOMAIN_RANGE_VEGA_SCALE_PROPERTIES = keys(NON_TYPE_DOMAIN_RANGE_VEGA_SCALE_PROPERTY_INDEX);
function scaleTypeSupportProperty(scaleType, propName) {
    switch (propName) {
        case 'type':
        case 'domain':
        case 'reverse':
        case 'range':
            return true;
        case 'scheme':
        case 'interpolate':
            return !['point', 'band', 'identity'].includes(scaleType);
        case 'bins':
            return !['point', 'band', 'identity', 'ordinal'].includes(scaleType);
        case 'round':
            return isContinuousToContinuous(scaleType) || scaleType === 'band' || scaleType === 'point';
        case 'padding':
        case 'rangeMin':
        case 'rangeMax':
            return isContinuousToContinuous(scaleType) || ['point', 'band'].includes(scaleType);
        case 'paddingOuter':
        case 'align':
            return ['point', 'band'].includes(scaleType);
        case 'paddingInner':
            return scaleType === 'band';
        case 'domainMax':
        case 'domainMid':
        case 'domainMin':
        case 'domainRaw':
        case 'clamp':
            return isContinuousToContinuous(scaleType);
        case 'nice':
            return isContinuousToContinuous(scaleType) || scaleType === 'quantize' || scaleType === 'threshold';
        case 'exponent':
            return scaleType === 'pow';
        case 'base':
            return scaleType === 'log';
        case 'constant':
            return scaleType === 'symlog';
        case 'zero':
            return (hasContinuousDomain(scaleType) &&
                !contains([
                    'log', // log scale cannot have zero value
                    'time',
                    'utc', // zero is not meaningful for time
                    'threshold', // threshold requires custom domain so zero does not matter
                    'quantile', // quantile depends on distribution so zero does not matter
                ], scaleType));
    }
}
/**
 * Returns undefined if the input channel supports the input scale property name
 */
function channelScalePropertyIncompatability(channel, propName) {
    switch (propName) {
        case 'interpolate':
        case 'scheme':
        case 'domainMid':
            if (!isColorChannel(channel)) {
                return cannotUseScalePropertyWithNonColor(propName);
            }
            return undefined;
        case 'align':
        case 'type':
        case 'bins':
        case 'domain':
        case 'domainMax':
        case 'domainMin':
        case 'domainRaw':
        case 'range':
        case 'base':
        case 'exponent':
        case 'constant':
        case 'nice':
        case 'padding':
        case 'paddingInner':
        case 'paddingOuter':
        case 'rangeMax':
        case 'rangeMin':
        case 'reverse':
        case 'round':
        case 'clamp':
        case 'zero':
            return undefined; // GOOD!
    }
}
function scaleTypeSupportDataType(specifiedType, fieldDefType) {
    if (contains([ORDINAL, NOMINAL], fieldDefType)) {
        return specifiedType === undefined || hasDiscreteDomain(specifiedType);
    }
    else if (fieldDefType === TEMPORAL) {
        return contains([ScaleType.TIME, ScaleType.UTC, undefined], specifiedType);
    }
    else if (fieldDefType === QUANTITATIVE) {
        return isQuantitative(specifiedType) || isContinuousToDiscrete(specifiedType) || specifiedType === undefined;
    }
    return true;
}
function channelSupportScaleType(channel, scaleType, hasNestedOffsetScale = false) {
    if (!isScaleChannel(channel)) {
        return false;
    }
    switch (channel) {
        case X:
        case Y:
        case XOFFSET:
        case YOFFSET:
        case THETA:
        case RADIUS:
            if (isContinuousToContinuous(scaleType)) {
                return true;
            }
            else if (scaleType === 'band') {
                return true;
            }
            else if (scaleType === 'point') {
                /*
                  Point scale can't be use if the position has a nested offset scale
                  because if there is a nested scale, then it's band.
                */
                return !hasNestedOffsetScale;
            }
            return false;
        case TIME:
            return contains(['linear', 'band'], scaleType);
        case SIZE: // TODO: size and opacity can support ordinal with more modification
        case STROKEWIDTH:
        case OPACITY:
        case FILLOPACITY:
        case STROKEOPACITY:
        case ANGLE:
            // Although it generally doesn't make sense to use band with size and opacity,
            // it can also work since we use band: 0.5 to get midpoint.
            return (isContinuousToContinuous(scaleType) ||
                isContinuousToDiscrete(scaleType) ||
                contains(['band', 'point', 'ordinal'], scaleType));
        case COLOR:
        case FILL:
        case STROKE:
            return scaleType !== 'band'; // band does not make sense with color
        case STROKEDASH:
        case SHAPE:
            return scaleType === 'ordinal' || isContinuousToDiscrete(scaleType);
    }
}

function isScaleInvalidDataIncludeAsValue(invalidDataMode) {
    return isObject(invalidDataMode) && 'value' in invalidDataMode;
}

/**
 * All types of primitive marks.
 */
const Mark = {
    arc: 'arc',
    area: 'area',
    bar: 'bar',
    image: 'image',
    line: 'line',
    point: 'point',
    rect: 'rect',
    rule: 'rule',
    text: 'text',
    tick: 'tick',
    trail: 'trail',
    circle: 'circle',
    square: 'square',
    geoshape: 'geoshape',
};
const ARC = Mark.arc;
const AREA = Mark.area;
const BAR = Mark.bar;
const IMAGE = Mark.image;
const LINE = Mark.line;
const POINT = Mark.point;
const RECT = Mark.rect;
const RULE = Mark.rule;
const TEXT = Mark.text;
const TICK = Mark.tick;
const TRAIL = Mark.trail;
const CIRCLE = Mark.circle;
const SQUARE = Mark.square;
const GEOSHAPE = Mark.geoshape;
function isPathMark(m) {
    return ['line', 'area', 'trail'].includes(m);
}
function isRectBasedMark(m) {
    return ['rect', 'bar', 'image', 'arc', 'tick' /* arc is rect/interval in polar coordinate */].includes(m);
}
const PRIMITIVE_MARKS = new Set(keys(Mark));
function isMarkDef(mark) {
    return hasProperty(mark, 'type');
}
const STROKE_CONFIG = [
    'stroke',
    'strokeWidth',
    'strokeDash',
    'strokeDashOffset',
    'strokeOpacity',
    'strokeJoin',
    'strokeMiterLimit',
];
const FILL_CONFIG = ['fill', 'fillOpacity'];
const FILL_STROKE_CONFIG = [...STROKE_CONFIG, ...FILL_CONFIG];
const VL_ONLY_MARK_CONFIG_INDEX = {
    color: 1,
    filled: 1,
    invalid: 1,
    order: 1,
    radius2: 1,
    theta2: 1,
    timeUnitBandSize: 1,
    timeUnitBandPosition: 1,
};
const VL_ONLY_MARK_CONFIG_PROPERTIES = keys(VL_ONLY_MARK_CONFIG_INDEX);
const VL_ONLY_RECT_CONFIG = [
    'binSpacing',
    'continuousBandSize',
    'discreteBandSize',
    'minBandSize',
];
const VL_ONLY_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX = {
    area: ['line', 'point'],
    bar: VL_ONLY_RECT_CONFIG,
    rect: VL_ONLY_RECT_CONFIG,
    line: ['point'],
    tick: ['bandSize', 'thickness', ...VL_ONLY_RECT_CONFIG],
};
const defaultMarkConfig = {
    color: '#4c78a8',
    invalid: 'break-paths-show-path-domains',
    timeUnitBandSize: 1,
};
const MARK_CONFIG_INDEX = {
    mark: 1,
    arc: 1,
    area: 1,
    bar: 1,
    circle: 1,
    image: 1,
    line: 1,
    point: 1,
    rect: 1,
    rule: 1,
    square: 1,
    text: 1,
    tick: 1,
    trail: 1,
    geoshape: 1,
};
const MARK_CONFIGS = keys(MARK_CONFIG_INDEX);
function isRelativeBandSize(o) {
    return hasProperty(o, 'band');
}
const BAR_CORNER_RADIUS_INDEX = {
    horizontal: ['cornerRadiusTopRight', 'cornerRadiusBottomRight'],
    vertical: ['cornerRadiusTopLeft', 'cornerRadiusTopRight'],
};
const DEFAULT_RECT_BAND_SIZE = 5;
const defaultRectConfig = {
    binSpacing: 0,
    continuousBandSize: DEFAULT_RECT_BAND_SIZE,
    minBandSize: 0.25,
    timeUnitBandPosition: 0.5,
};
const defaultBarConfig = {
    ...defaultRectConfig,
    binSpacing: 1,
};
const defaultTickConfig = {
    ...defaultRectConfig,
    thickness: 1,
};
function getMarkType(m) {
    return isMarkDef(m) ? m.type : m;
}

function normalizeInvalidDataMode(mode, { isPath }) {
    if (mode === undefined || mode === 'break-paths-show-path-domains') {
        return isPath ? 'break-paths-show-domains' : 'filter';
    }
    else if (mode === null) {
        return 'show';
    }
    return mode;
}

function getScaleInvalidDataMode({ markDef, config, scaleChannel, scaleType, isCountAggregate, }) {
    if (!scaleType || !hasContinuousDomain(scaleType) || isCountAggregate) {
        // - Discrete scales can always display null as another category
        // - Count cannot output null values
        return 'always-valid';
    }
    const invalidMode = normalizeInvalidDataMode(getMarkPropOrConfig('invalid', markDef, config), {
        isPath: isPathMark(markDef.type),
    });
    const scaleOutputForInvalid = config.scale?.invalid?.[scaleChannel];
    if (scaleOutputForInvalid !== undefined) {
        // Regardless of the current invalid mode, if the channel has a default value, we consider the field valid.
        return 'show';
    }
    return invalidMode;
}
function shouldBreakPath(mode) {
    return mode === 'break-paths-filter-domains' || mode === 'break-paths-show-domains';
}

function scaledZeroOrMinOrMax({ scaleName, scale, mode }) {
    const domain = `domain('${scaleName}')`;
    if (!scale || !scaleName) {
        return undefined;
    }
    const min = `${domain}[0]`;
    const max = `peek(${domain})`; // peek = the last item of the array
    // If there is a scale (and hence its name)
    const domainHasZero = scale.domainHasZero();
    // zeroOrMin or zeroOrMax mode
    if (domainHasZero === 'definitely') {
        return {
            scale: scaleName,
            value: 0,
        };
    }
    else if (domainHasZero === 'maybe') {
        const nonZeroValue = mode === 'zeroOrMin' ? min : max;
        return { signal: `scale('${scaleName}', inrange(0, ${domain}) ? 0 : ${nonZeroValue})` };
    }
    else {
        // domainHasZero === 'definitely-not'
        return { signal: `scale('${scaleName}', ${mode === 'zeroOrMin' ? min : max})` };
    }
}

function getConditionalValueRefForIncludingInvalidValue({ scaleChannel, channelDef, scale, scaleName, markDef, config, }) {
    const scaleType = scale?.get('type');
    const fieldDef = getFieldDef(channelDef);
    const isCountAggregate = isCountingAggregateOp(fieldDef?.aggregate);
    const invalidDataMode = getScaleInvalidDataMode({
        scaleChannel,
        markDef,
        config,
        scaleType,
        isCountAggregate,
    });
    if (fieldDef && invalidDataMode === 'show') {
        const includeAs = config.scale.invalid?.[scaleChannel] ?? 'zero-or-min';
        return {
            test: fieldValidPredicate(vgField(fieldDef, { expr: 'datum' }), false),
            ...refForInvalidValues(includeAs, scale, scaleName),
        };
    }
    return undefined;
}
function refForInvalidValues(includeAs, scale, scaleName) {
    if (isScaleInvalidDataIncludeAsValue(includeAs)) {
        const { value } = includeAs;
        return isSignalRef(value) ? { signal: value.signal } : { value };
    }
    return scaledZeroOrMinOrMax({
        scale,
        scaleName,
        mode: 'zeroOrMin',
    });
}

function midPointRefWithPositionInvalidTest(params) {
    const { channel, channelDef, markDef, scale, scaleName, config } = params;
    const scaleChannel = getMainRangeChannel(channel);
    const mainRef = midPoint(params);
    const valueRefForIncludingInvalid = getConditionalValueRefForIncludingInvalidValue({
        scaleChannel,
        channelDef,
        scale,
        scaleName,
        markDef,
        config,
    });
    return valueRefForIncludingInvalid !== undefined ? [valueRefForIncludingInvalid, mainRef] : mainRef;
}
function datumDefToExpr(datumDef) {
    const { datum } = datumDef;
    if (isDateTime(datum)) {
        return dateTimeToExpr(datum);
    }
    return `${stringify$1(datum)}`;
}
function valueRefForFieldOrDatumDef(fieldDef, scaleName, opt, encode) {
    const ref = {};
    if (scaleName) {
        ref.scale = scaleName;
    }
    if (isDatumDef(fieldDef)) {
        const { datum } = fieldDef;
        if (isDateTime(datum)) {
            ref.signal = dateTimeToExpr(datum);
        }
        else if (isSignalRef(datum)) {
            ref.signal = datum.signal;
        }
        else if (isExprRef(datum)) {
            ref.signal = datum.expr;
        }
        else {
            ref.value = datum;
        }
    }
    else {
        ref.field = vgField(fieldDef, opt);
    }
    if (encode) {
        const { offset, band } = encode;
        if (offset) {
            ref.offset = offset;
        }
        if (band) {
            ref.band = band;
        }
    }
    return ref;
}
/**
 * Signal that returns the middle of a bin from start and end field. Should only be used with x and y.
 */
function interpolatedSignalRef({ scaleName, fieldOrDatumDef, fieldOrDatumDef2, offset, startSuffix, endSuffix = 'end', bandPosition = 0.5, }) {
    const expr = !isSignalRef(bandPosition) && 0 < bandPosition && bandPosition < 1 ? 'datum' : undefined;
    const start = vgField(fieldOrDatumDef, { expr, suffix: startSuffix });
    const end = fieldOrDatumDef2 !== undefined
        ? vgField(fieldOrDatumDef2, { expr })
        : vgField(fieldOrDatumDef, { suffix: endSuffix, expr });
    const ref = {};
    if (bandPosition === 0 || bandPosition === 1) {
        ref.scale = scaleName;
        const field = bandPosition === 0 ? start : end;
        ref.field = field;
    }
    else {
        const datum = isSignalRef(bandPosition)
            ? `(1-${bandPosition.signal}) * ${start} + ${bandPosition.signal} * ${end}`
            : `${1 - bandPosition} * ${start} + ${bandPosition} * ${end}`;
        ref.signal = `scale("${scaleName}", ${datum})`;
    }
    if (offset) {
        ref.offset = offset;
    }
    return ref;
}
function binSizeExpr({ scaleName, fieldDef }) {
    const start = vgField(fieldDef, { expr: 'datum' });
    const end = vgField(fieldDef, { expr: 'datum', suffix: 'end' });
    return `abs(scale("${scaleName}", ${end}) - scale("${scaleName}", ${start}))`;
}
/**
 * @returns {VgValueRef} Value Ref for xc / yc or mid point for other channels.
 */
function midPoint({ channel, channelDef, channel2Def, markDef, config, scaleName, scale, stack, offset, defaultRef, bandPosition, }) {
    // TODO: datum support
    if (channelDef) {
        /* istanbul ignore else */
        if (isFieldOrDatumDef(channelDef)) {
            const scaleType = scale?.get('type');
            if (isTypedFieldDef(channelDef)) {
                bandPosition ??= getBandPosition({
                    fieldDef: channelDef,
                    fieldDef2: channel2Def,
                    markDef,
                    config,
                });
                const { bin, timeUnit, type } = channelDef;
                if (isBinning(bin) || (bandPosition && timeUnit && type === TEMPORAL)) {
                    // Use middle only for x an y to place marks in the center between start and end of the bin range.
                    // We do not use the mid point for other channels (e.g. size) so that properties of legends and marks match.
                    if (stack?.impute) {
                        // For stack, we computed bin_mid so we can impute.
                        return valueRefForFieldOrDatumDef(channelDef, scaleName, { binSuffix: 'mid' }, { offset });
                    }
                    if (bandPosition && !hasDiscreteDomain(scaleType)) {
                        // if band = 0, no need to call interpolation
                        // For non-stack, we can just calculate bin mid on the fly using signal.
                        return interpolatedSignalRef({ scaleName, fieldOrDatumDef: channelDef, bandPosition, offset });
                    }
                    return valueRefForFieldOrDatumDef(channelDef, scaleName, binRequiresRange(channelDef, channel) ? { binSuffix: 'range' } : {}, {
                        offset,
                    });
                }
                else if (isBinned(bin)) {
                    if (isFieldDef(channel2Def)) {
                        return interpolatedSignalRef({
                            scaleName,
                            fieldOrDatumDef: channelDef,
                            fieldOrDatumDef2: channel2Def,
                            bandPosition,
                            offset,
                        });
                    }
                    else {
                        const channel2 = channel === X ? X2 : Y2;
                        warn(channelRequiredForBinned(channel2));
                    }
                }
            }
            return valueRefForFieldOrDatumDef(channelDef, scaleName, hasDiscreteDomain(scaleType) ? { binSuffix: 'range' } : {}, // no need for bin suffix if there is no scale
            {
                offset,
                // For band, to get mid point, need to offset by half of the band
                band: scaleType === 'band' ? (bandPosition ?? channelDef.bandPosition ?? 0.5) : undefined,
            });
        }
        else if (isValueDef(channelDef)) {
            const value = channelDef.value;
            const offsetMixins = offset ? { offset } : {};
            return { ...widthHeightValueOrSignalRef(channel, value), ...offsetMixins };
        }
        // If channelDef is neither field def or value def, it's a condition-only def.
        // In such case, we will use default ref.
    }
    if (isFunction(defaultRef)) {
        defaultRef = defaultRef();
    }
    if (defaultRef) {
        // for non-position, ref could be undefined.
        return {
            ...defaultRef,
            // only include offset when it is non-zero (zero = no offset)
            ...(offset ? { offset } : {}),
        };
    }
    return defaultRef;
}
/**
 * Convert special "width" and "height" values in Vega-Lite into Vega value ref.
 */
function widthHeightValueOrSignalRef(channel, value) {
    if (contains(['x', 'x2'], channel) && value === 'width') {
        return { field: { group: 'width' } };
    }
    else if (contains(['y', 'y2'], channel) && value === 'height') {
        return { field: { group: 'height' } };
    }
    return signalOrValueRef(value);
}

function isCustomFormatType(formatType) {
    return formatType && formatType !== 'number' && formatType !== 'time';
}
function customFormatExpr(formatType, field, format) {
    return `${formatType}(${field}${format ? `, ${stringify$1(format)}` : ''})`;
}
function formatSignalRef({ fieldOrDatumDef, format, formatType, expr, normalizeStack, config, }) {
    if (isCustomFormatType(formatType)) {
        return formatCustomType({
            fieldOrDatumDef,
            format,
            formatType,
            expr,
            config,
        });
    }
    const field = fieldToFormat(fieldOrDatumDef, expr, normalizeStack);
    const type = channelDefType(fieldOrDatumDef);
    if (format === undefined && formatType === undefined && config.customFormatTypes) {
        if (type === 'quantitative') {
            if (normalizeStack && config.normalizedNumberFormatType)
                return formatCustomType({
                    fieldOrDatumDef,
                    format: config.normalizedNumberFormat,
                    formatType: config.normalizedNumberFormatType,
                    expr,
                    config,
                });
            if (config.numberFormatType) {
                return formatCustomType({
                    fieldOrDatumDef,
                    format: config.numberFormat,
                    formatType: config.numberFormatType,
                    expr,
                    config,
                });
            }
        }
        if (type === 'temporal' &&
            config.timeFormatType &&
            isFieldDef(fieldOrDatumDef) &&
            fieldOrDatumDef.timeUnit === undefined) {
            return formatCustomType({
                fieldOrDatumDef,
                format: config.timeFormat,
                formatType: config.timeFormatType,
                expr,
                config,
            });
        }
    }
    function getTimeDef(def) {
        if (!isFieldDef(def)) {
            return {
                unit: undefined,
                utc: undefined,
            };
        }
        return normalizeTimeUnit(def.timeUnit) || {};
    }
    if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
        const { unit: timeUnit, utc: isUTCUnit } = getTimeDef(fieldOrDatumDef);
        const signal = timeFormatExpression({
            field,
            timeUnit,
            format,
            formatType: config.timeFormatType,
            rawTimeFormat: config.timeFormat,
            isUTCScale: isUTCUnit || (isScaleFieldDef(fieldOrDatumDef) && fieldOrDatumDef.scale?.type === ScaleType.UTC),
        });
        return signal ? { signal } : undefined;
    }
    format = numberFormat({ type, specifiedFormat: format, config, normalizeStack });
    if (isFieldDef(fieldOrDatumDef) && isBinning(fieldOrDatumDef.bin)) {
        const endField = vgField(fieldOrDatumDef, { expr, binSuffix: 'end' });
        return {
            signal: binFormatExpression(field, endField, format, formatType, config),
        };
    }
    else if (format || channelDefType(fieldOrDatumDef) === 'quantitative') {
        return {
            signal: `${formatExpr(field, format)}`,
        };
    }
    else {
        return { signal: `isValid(${field}) ? ${field} : ""+${field}` };
    }
}
function fieldToFormat(fieldOrDatumDef, expr, normalizeStack) {
    if (isFieldDef(fieldOrDatumDef)) {
        if (normalizeStack) {
            return `${vgField(fieldOrDatumDef, { expr, suffix: 'end' })}-${vgField(fieldOrDatumDef, {
                expr,
                suffix: 'start',
            })}`;
        }
        else {
            return vgField(fieldOrDatumDef, { expr });
        }
    }
    else {
        return datumDefToExpr(fieldOrDatumDef);
    }
}
function formatCustomType({ fieldOrDatumDef, format, formatType, expr, normalizeStack, config, field, }) {
    field ??= fieldToFormat(fieldOrDatumDef, expr, normalizeStack);
    if (field !== 'datum.value' && // For axis/legend, we can't correctly know the end of the bin from `datum`
        isFieldDef(fieldOrDatumDef) &&
        isBinning(fieldOrDatumDef.bin)) {
        const endField = vgField(fieldOrDatumDef, { expr, binSuffix: 'end' });
        return {
            signal: binFormatExpression(field, endField, format, formatType, config),
        };
    }
    return { signal: customFormatExpr(formatType, field, format) };
}
function guideFormat(fieldOrDatumDef, type, format, formatType, config, omitTimeFormatConfig) {
    if (isString(formatType) && isCustomFormatType(formatType)) {
        return undefined; // handled in encode block
    }
    else if (format === undefined && formatType === undefined && config.customFormatTypes) {
        if (channelDefType(fieldOrDatumDef) === 'quantitative') {
            if (config.normalizedNumberFormatType &&
                isPositionFieldOrDatumDef(fieldOrDatumDef) &&
                fieldOrDatumDef.stack === 'normalize') {
                return undefined; // handled in encode block
            }
            if (config.numberFormatType) {
                return undefined; // handled in encode block
            }
        }
    }
    if (isPositionFieldOrDatumDef(fieldOrDatumDef) &&
        fieldOrDatumDef.stack === 'normalize' &&
        config.normalizedNumberFormat) {
        return numberFormat({
            type: 'quantitative',
            config,
            normalizeStack: true,
        });
    }
    if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
        const timeUnit = isFieldDef(fieldOrDatumDef) ? normalizeTimeUnit(fieldOrDatumDef.timeUnit)?.unit : undefined;
        if (timeUnit === undefined && config.customFormatTypes && config.timeFormatType) {
            return undefined; // hanlded in encode block
        }
        return timeFormat({ specifiedFormat: format, timeUnit, config, omitTimeFormatConfig });
    }
    return numberFormat({ type, specifiedFormat: format, config });
}
function guideFormatType(formatType, fieldOrDatumDef, scaleType) {
    if (formatType && (isSignalRef(formatType) || formatType === 'number' || formatType === 'time')) {
        return formatType;
    }
    if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef) && scaleType !== 'time' && scaleType !== 'utc') {
        return isFieldDef(fieldOrDatumDef) && normalizeTimeUnit(fieldOrDatumDef?.timeUnit)?.utc ? 'utc' : 'time';
    }
    return undefined;
}
/**
 * Returns number format for a fieldDef.
 */
function numberFormat({ type, specifiedFormat, config, normalizeStack, }) {
    // Specified format in axis/legend has higher precedence than fieldDef.format
    if (isString(specifiedFormat)) {
        return specifiedFormat;
    }
    if (type === QUANTITATIVE) {
        // we only apply the default if the field is quantitative
        return normalizeStack ? config.normalizedNumberFormat : config.numberFormat;
    }
    return undefined;
}
/**
 * Returns time format for a fieldDef for use in guides.
 */
function timeFormat({ specifiedFormat, timeUnit, config, omitTimeFormatConfig, }) {
    if (specifiedFormat) {
        return specifiedFormat;
    }
    if (timeUnit) {
        return {
            signal: timeUnitSpecifierExpression(timeUnit),
        };
    }
    return omitTimeFormatConfig ? undefined : config.timeFormat;
}
function formatExpr(field, format) {
    return `format(${field}, "${format || ''}")`;
}
function binNumberFormatExpr(field, format, formatType, config) {
    if (isCustomFormatType(formatType)) {
        return customFormatExpr(formatType, field, format);
    }
    return formatExpr(field, (isString(format) ? format : undefined) ?? config.numberFormat);
}
function binFormatExpression(startField, endField, format, formatType, config) {
    if (format === undefined && formatType === undefined && config.customFormatTypes && config.numberFormatType) {
        return binFormatExpression(startField, endField, config.numberFormat, config.numberFormatType, config);
    }
    const start = binNumberFormatExpr(startField, format, formatType, config);
    const end = binNumberFormatExpr(endField, format, formatType, config);
    return `${fieldValidPredicate(startField, false)} ? "null" : ${start} + "${BIN_RANGE_DELIMITER}" + ${end}`;
}
/**
 * Returns the time expression used for axis/legend labels or text mark for a temporal field
 */
function timeFormatExpression({ field, timeUnit, format, formatType, rawTimeFormat, isUTCScale, }) {
    if (!timeUnit || format) {
        // If there is no time unit, or if user explicitly specifies format for axis/legend/text.
        if (!timeUnit && formatType) {
            return `${formatType}(${field}, ${stringify$1(format)})`;
        }
        format = isString(format) ? format : rawTimeFormat; // only use provided timeFormat if there is no timeUnit.
        return `${isUTCScale ? 'utc' : 'time'}Format(${field}, ${stringify$1(format)})`;
    }
    else {
        return formatExpression(timeUnit, field, isUTCScale);
    }
}

const DEFAULT_SORT_OP = 'min';
const SORT_BY_CHANNEL_INDEX = {
    x: 1,
    y: 1,
    color: 1,
    fill: 1,
    stroke: 1,
    strokeWidth: 1,
    size: 1,
    shape: 1,
    fillOpacity: 1,
    strokeOpacity: 1,
    opacity: 1,
    text: 1,
};
function isSortByChannel(c) {
    return has$1(SORT_BY_CHANNEL_INDEX, c);
}
function isSortByEncoding(sort) {
    return hasProperty(sort, 'encoding');
}
function isSortField(sort) {
    return sort && (sort.op === 'count' || hasProperty(sort, 'field'));
}
function isSortArray(sort) {
    return sort && isArray(sort);
}

function isFacetMapping(f) {
    return hasProperty(f, 'row') || hasProperty(f, 'column');
}
function isFacetFieldDef(channelDef) {
    return hasProperty(channelDef, 'header');
}
function isFacetSpec(spec) {
    return hasProperty(spec, 'facet');
}

function isConditionalParameter(c) {
    return hasProperty(c, 'param');
}
function isRepeatRef(field) {
    return !isString(field) && hasProperty(field, 'repeat');
}
function toFieldDefBase(fieldDef) {
    const { field, timeUnit, bin, aggregate } = fieldDef;
    return {
        ...(timeUnit ? { timeUnit } : {}),
        ...(bin ? { bin } : {}),
        ...(aggregate ? { aggregate } : {}),
        field,
    };
}
function isSortableFieldDef(fieldDef) {
    return hasProperty(fieldDef, 'sort');
}
function getBandPosition({ fieldDef, fieldDef2, markDef: mark, config, }) {
    if (isFieldOrDatumDef(fieldDef) && fieldDef.bandPosition !== undefined) {
        return fieldDef.bandPosition;
    }
    if (isFieldDef(fieldDef)) {
        const { timeUnit, bin } = fieldDef;
        if (timeUnit && !fieldDef2) {
            return getMarkConfig('timeUnitBandPosition', mark, config);
        }
        else if (isBinning(bin)) {
            return 0.5;
        }
    }
    return undefined;
}
function getBandSize({ channel, fieldDef, fieldDef2, markDef: mark, config, scaleType, useVlSizeChannel, }) {
    const sizeChannel = getSizeChannel(channel);
    const size = getMarkPropOrConfig(useVlSizeChannel ? 'size' : sizeChannel, mark, config, {
        vgChannel: sizeChannel,
    });
    if (size !== undefined) {
        return size;
    }
    if (isFieldDef(fieldDef)) {
        const { timeUnit, bin } = fieldDef;
        if (timeUnit && !fieldDef2) {
            return { band: getMarkConfig('timeUnitBandSize', mark, config) };
        }
        else if (isBinning(bin) && !hasDiscreteDomain(scaleType)) {
            return { band: 1 };
        }
    }
    if (isRectBasedMark(mark.type)) {
        if (scaleType) {
            if (hasDiscreteDomain(scaleType)) {
                return config[mark.type]?.discreteBandSize || { band: 1 };
            }
            else {
                return config[mark.type]?.continuousBandSize;
            }
        }
        return config[mark.type]?.discreteBandSize;
    }
    return undefined;
}
function hasBandEnd(fieldDef, fieldDef2, markDef, config) {
    if (isBinning(fieldDef.bin) || (fieldDef.timeUnit && isTypedFieldDef(fieldDef) && fieldDef.type === 'temporal')) {
        // Need to check bandPosition because non-rect marks (e.g., point) with timeUnit
        // doesn't have to use bandEnd if there is no bandPosition.
        return getBandPosition({ fieldDef, fieldDef2, markDef, config }) !== undefined;
    }
    return false;
}
function isOrderOnlyDef(orderDef) {
    return hasProperty(orderDef, 'sort') && !hasProperty(orderDef, 'field');
}
function isConditionalDef(channelDef) {
    return hasProperty(channelDef, 'condition');
}
/**
 * Return if a channelDef is a ConditionalValueDef with ConditionFieldDef
 */
function hasConditionalFieldDef(channelDef) {
    const condition = channelDef?.['condition'];
    return !!condition && !isArray(condition) && isFieldDef(condition);
}
function hasConditionalFieldOrDatumDef(channelDef) {
    const condition = channelDef?.['condition'];
    return !!condition && !isArray(condition) && isFieldOrDatumDef(condition);
}
function hasConditionalValueDef(channelDef) {
    const condition = channelDef?.['condition'];
    return !!condition && (isArray(condition) || isValueDef(condition));
}
function isFieldDef(channelDef) {
    return hasProperty(channelDef, 'field') || channelDef?.aggregate === 'count';
}
function channelDefType(channelDef) {
    return channelDef?.['type'];
}
function isDatumDef(channelDef) {
    return hasProperty(channelDef, 'datum');
}
function isContinuousFieldOrDatumDef(cd) {
    // TODO: make datum support DateTime object
    return (isTypedFieldDef(cd) && !isDiscrete(cd)) || isNumericDataDef(cd);
}
function isUnbinnedQuantitativeFieldOrDatumDef(cd) {
    // TODO: make datum support DateTime object
    return (isTypedFieldDef(cd) && cd.type === 'quantitative' && !cd.bin) || isNumericDataDef(cd);
}
function isNumericDataDef(cd) {
    return isDatumDef(cd) && isNumber$1(cd.datum);
}
function isFieldOrDatumDef(channelDef) {
    return isFieldDef(channelDef) || isDatumDef(channelDef);
}
function isTypedFieldDef(channelDef) {
    return (channelDef &&
        (hasProperty(channelDef, 'field') || channelDef['aggregate'] === 'count') &&
        hasProperty(channelDef, 'type'));
}
function isValueDef(channelDef) {
    return hasProperty(channelDef, 'value');
}
function isScaleFieldDef(channelDef) {
    return hasProperty(channelDef, 'scale') || hasProperty(channelDef, 'sort');
}
function isPositionFieldOrDatumDef(channelDef) {
    return hasProperty(channelDef, 'axis') || hasProperty(channelDef, 'stack') || hasProperty(channelDef, 'impute');
}
function isMarkPropFieldOrDatumDef(channelDef) {
    return hasProperty(channelDef, 'legend');
}
function isStringFieldOrDatumDef(channelDef) {
    return hasProperty(channelDef, 'format') || hasProperty(channelDef, 'formatType');
}
function toStringFieldDef(fieldDef) {
    // omit properties that don't exist in string field defs
    return omit(fieldDef, ['legend', 'axis', 'header', 'scale']);
}
function isOpFieldDef(fieldDef) {
    return hasProperty(fieldDef, 'op');
}
/**
 * Get a Vega field reference from a Vega-Lite field def.
 */
function vgField(fieldDef, opt = {}) {
    let field = fieldDef.field;
    const prefix = opt.prefix;
    let suffix = opt.suffix;
    let argAccessor = ''; // for accessing argmin/argmax field at the end without getting escaped
    if (isCount(fieldDef)) {
        field = internalField('count');
    }
    else {
        let fn;
        if (!opt.nofn) {
            if (isOpFieldDef(fieldDef)) {
                fn = fieldDef.op;
            }
            else {
                const { bin, aggregate, timeUnit } = fieldDef;
                if (isBinning(bin)) {
                    fn = binToString(bin);
                    suffix = (opt.binSuffix ?? '') + (opt.suffix ?? '');
                }
                else if (aggregate) {
                    if (isArgmaxDef(aggregate)) {
                        argAccessor = `["${field}"]`;
                        field = `argmax_${aggregate.argmax}`;
                    }
                    else if (isArgminDef(aggregate)) {
                        argAccessor = `["${field}"]`;
                        field = `argmin_${aggregate.argmin}`;
                    }
                    else {
                        fn = String(aggregate);
                    }
                }
                else if (timeUnit && !isBinnedTimeUnit(timeUnit)) {
                    fn = timeUnitToString(timeUnit);
                    suffix = ((!['range', 'mid'].includes(opt.binSuffix) && opt.binSuffix) || '') + (opt.suffix ?? '');
                }
            }
        }
        if (fn) {
            field = field ? `${fn}_${field}` : fn;
        }
    }
    if (suffix) {
        field = `${field}_${suffix}`;
    }
    if (prefix) {
        field = `${prefix}_${field}`;
    }
    if (opt.forAs) {
        return removePathFromField(field);
    }
    else if (opt.expr) {
        // Expression to access flattened field. No need to escape dots.
        return flatAccessWithDatum(field, opt.expr) + argAccessor;
    }
    else {
        // We flattened all fields so paths should have become dot.
        return replacePathInField(field) + argAccessor;
    }
}
function isDiscrete(def) {
    switch (def.type) {
        case 'nominal':
        case 'ordinal':
        case 'geojson':
            return true;
        case 'quantitative':
            return isFieldDef(def) && !!def.bin;
        case 'temporal':
            return false;
    }
    throw new Error(invalidFieldType(def.type));
}
function isDiscretizing(def) {
    return isScaleFieldDef(def) && isContinuousToDiscrete(def.scale?.type);
}
function isCount(fieldDef) {
    return fieldDef.aggregate === 'count';
}
function verbalTitleFormatter(fieldDef, config) {
    const { field, bin, timeUnit, aggregate } = fieldDef;
    if (aggregate === 'count') {
        return config.countTitle;
    }
    else if (isBinning(bin)) {
        return `${field} (binned)`;
    }
    else if (timeUnit && !isBinnedTimeUnit(timeUnit)) {
        const unit = normalizeTimeUnit(timeUnit)?.unit;
        if (unit) {
            return `${field} (${getTimeUnitParts(unit).join('-')})`;
        }
    }
    else if (aggregate) {
        if (isArgmaxDef(aggregate)) {
            return `${field} for max ${aggregate.argmax}`;
        }
        else if (isArgminDef(aggregate)) {
            return `${field} for min ${aggregate.argmin}`;
        }
        else {
            return `${titleCase(aggregate)} of ${field}`;
        }
    }
    return field;
}
function functionalTitleFormatter(fieldDef) {
    const { aggregate, bin, timeUnit, field } = fieldDef;
    if (isArgmaxDef(aggregate)) {
        return `${field} for argmax(${aggregate.argmax})`;
    }
    else if (isArgminDef(aggregate)) {
        return `${field} for argmin(${aggregate.argmin})`;
    }
    const timeUnitParams = timeUnit && !isBinnedTimeUnit(timeUnit) ? normalizeTimeUnit(timeUnit) : undefined;
    const fn = aggregate || timeUnitParams?.unit || (timeUnitParams?.maxbins && 'timeunit') || (isBinning(bin) && 'bin');
    return fn ? `${fn.toUpperCase()}(${field})` : field;
}
const defaultTitleFormatter = (fieldDef, config) => {
    switch (config.fieldTitle) {
        case 'plain':
            return fieldDef.field;
        case 'functional':
            return functionalTitleFormatter(fieldDef);
        default:
            return verbalTitleFormatter(fieldDef, config);
    }
};
let titleFormatter = defaultTitleFormatter;
function setTitleFormatter(formatter) {
    titleFormatter = formatter;
}
function resetTitleFormatter() {
    setTitleFormatter(defaultTitleFormatter);
}
function title(fieldOrDatumDef, config, { allowDisabling, includeDefault = true }) {
    const guideTitle = getGuide(fieldOrDatumDef)?.title;
    if (!isFieldDef(fieldOrDatumDef)) {
        return guideTitle ?? fieldOrDatumDef.title;
    }
    const fieldDef = fieldOrDatumDef;
    const def = includeDefault ? defaultTitle(fieldDef, config) : undefined;
    if (allowDisabling) {
        return getFirstDefined(guideTitle, fieldDef.title, def);
    }
    else {
        return guideTitle ?? fieldDef.title ?? def;
    }
}
function getGuide(fieldDef) {
    if (isPositionFieldOrDatumDef(fieldDef) && fieldDef.axis) {
        return fieldDef.axis;
    }
    else if (isMarkPropFieldOrDatumDef(fieldDef) && fieldDef.legend) {
        return fieldDef.legend;
    }
    else if (isFacetFieldDef(fieldDef) && fieldDef.header) {
        return fieldDef.header;
    }
    return undefined;
}
function defaultTitle(fieldDef, config) {
    return titleFormatter(fieldDef, config);
}
function getFormatMixins(fieldDef) {
    if (isStringFieldOrDatumDef(fieldDef)) {
        const { format, formatType } = fieldDef;
        return { format, formatType };
    }
    else {
        const guide = getGuide(fieldDef) ?? {};
        const { format, formatType } = guide;
        return { format, formatType };
    }
}
function defaultType$2(fieldDef, channel) {
    switch (channel) {
        case 'latitude':
        case 'longitude':
            return 'quantitative';
        case 'row':
        case 'column':
        case 'facet':
        case 'shape':
        case 'strokeDash':
            return 'nominal';
        case 'order':
            return 'ordinal';
    }
    if (isSortableFieldDef(fieldDef) && isArray(fieldDef.sort)) {
        return 'ordinal';
    }
    const { aggregate, bin, timeUnit } = fieldDef;
    if (timeUnit) {
        return 'temporal';
    }
    if (bin || (aggregate && !isArgmaxDef(aggregate) && !isArgminDef(aggregate))) {
        return 'quantitative';
    }
    if (isScaleFieldDef(fieldDef) && fieldDef.scale?.type) {
        switch (SCALE_CATEGORY_INDEX[fieldDef.scale.type]) {
            case 'numeric':
            case 'discretizing':
                return 'quantitative';
            case 'time':
                return 'temporal';
        }
    }
    return 'nominal';
}
/**
 * Returns the fieldDef -- either from the outer channelDef or from the condition of channelDef.
 * @param channelDef
 */
function getFieldDef(channelDef) {
    if (isFieldDef(channelDef)) {
        return channelDef;
    }
    else if (hasConditionalFieldDef(channelDef)) {
        return channelDef.condition;
    }
    return undefined;
}
function getFieldOrDatumDef(channelDef) {
    if (isFieldOrDatumDef(channelDef)) {
        return channelDef;
    }
    else if (hasConditionalFieldOrDatumDef(channelDef)) {
        return channelDef.condition;
    }
    return undefined;
}
/**
 * Convert type to full, lowercase type, or augment the fieldDef with a default type if missing.
 */
function initChannelDef(channelDef, channel, config, opt = {}) {
    if (isPrimitive(channelDef)) {
        const primitiveType = isString(channelDef) ? 'string' : isNumber$1(channelDef) ? 'number' : 'boolean';
        warn(primitiveChannelDef(channel, primitiveType, channelDef));
        return { value: channelDef };
    }
    // If a fieldDef contains a field, we need type.
    if (isFieldOrDatumDef(channelDef)) {
        return initFieldOrDatumDef(channelDef, channel, config, opt);
    }
    else if (hasConditionalFieldOrDatumDef(channelDef)) {
        return {
            ...channelDef,
            // Need to cast as normalizeFieldDef normally return FieldDef, but here we know that it is definitely Condition<FieldDef>
            condition: initFieldOrDatumDef(channelDef.condition, channel, config, opt),
        };
    }
    return channelDef;
}
function initFieldOrDatumDef(fd, channel, config, opt) {
    if (isStringFieldOrDatumDef(fd)) {
        const { format, formatType, ...rest } = fd;
        if (isCustomFormatType(formatType) && !config.customFormatTypes) {
            warn(customFormatTypeNotAllowed(channel));
            return initFieldOrDatumDef(rest, channel, config, opt);
        }
    }
    else {
        const guideType = isPositionFieldOrDatumDef(fd)
            ? 'axis'
            : isMarkPropFieldOrDatumDef(fd)
                ? 'legend'
                : isFacetFieldDef(fd)
                    ? 'header'
                    : null;
        if (guideType && fd[guideType]) {
            const { format, formatType, ...newGuide } = fd[guideType];
            if (isCustomFormatType(formatType) && !config.customFormatTypes) {
                warn(customFormatTypeNotAllowed(channel));
                return initFieldOrDatumDef({ ...fd, [guideType]: newGuide }, channel, config, opt);
            }
        }
    }
    if (isFieldDef(fd)) {
        return initFieldDef(fd, channel, opt);
    }
    return initDatumDef(fd);
}
function initDatumDef(datumDef) {
    let type = datumDef['type'];
    if (type) {
        return datumDef;
    }
    const { datum } = datumDef;
    type = isNumber$1(datum) ? 'quantitative' : isString(datum) ? 'nominal' : isDateTime(datum) ? 'temporal' : undefined;
    return { ...datumDef, type };
}
function initFieldDef(fd, channel, { compositeMark = false } = {}) {
    const { aggregate, timeUnit, bin, field } = fd;
    const fieldDef = { ...fd };
    // Drop invalid aggregate
    if (!compositeMark && aggregate && !isAggregateOp(aggregate) && !isArgmaxDef(aggregate) && !isArgminDef(aggregate)) {
        warn(invalidAggregate(aggregate));
        delete fieldDef.aggregate;
    }
    // Normalize Time Unit
    if (timeUnit) {
        fieldDef.timeUnit = normalizeTimeUnit(timeUnit);
    }
    if (field) {
        fieldDef.field = `${field}`;
    }
    // Normalize bin
    if (isBinning(bin)) {
        fieldDef.bin = normalizeBin(bin, channel);
    }
    if (isBinned(bin) && !isXorY(channel)) {
        warn(channelShouldNotBeUsedForBinned(channel));
    }
    // Normalize Type
    if (isTypedFieldDef(fieldDef)) {
        const { type } = fieldDef;
        const fullType = getFullName(type);
        if (type !== fullType) {
            // convert short type to full type
            fieldDef.type = fullType;
        }
        if (type !== 'quantitative') {
            if (isCountingAggregateOp(aggregate)) {
                warn(invalidFieldTypeForCountAggregate(type, aggregate));
                fieldDef.type = 'quantitative';
            }
        }
    }
    else if (!isSecondaryRangeChannel(channel)) {
        // If type is empty / invalid, then augment with default type
        const newType = defaultType$2(fieldDef, channel);
        fieldDef['type'] = newType;
    }
    if (isTypedFieldDef(fieldDef)) {
        const { compatible, warning } = channelCompatibility(fieldDef, channel) || {};
        if (compatible === false) {
            warn(warning);
        }
    }
    if (isSortableFieldDef(fieldDef) && isString(fieldDef.sort)) {
        const { sort } = fieldDef;
        if (isSortByChannel(sort)) {
            return {
                ...fieldDef,
                sort: { encoding: sort },
            };
        }
        const sub = sort.substring(1);
        if (sort.charAt(0) === '-' && isSortByChannel(sub)) {
            return {
                ...fieldDef,
                sort: { encoding: sub, order: 'descending' },
            };
        }
    }
    if (isFacetFieldDef(fieldDef)) {
        const { header } = fieldDef;
        if (header) {
            const { orient, ...rest } = header;
            if (orient) {
                return {
                    ...fieldDef,
                    header: {
                        ...rest,
                        labelOrient: header.labelOrient || orient,
                        titleOrient: header.titleOrient || orient,
                    },
                };
            }
        }
    }
    return fieldDef;
}
function normalizeBin(bin, channel) {
    if (isBoolean$2(bin)) {
        return { maxbins: autoMaxBins(channel) };
    }
    else if (bin === 'binned') {
        return {
            binned: true,
        };
    }
    else if (!bin.maxbins && !bin.step) {
        return { ...bin, maxbins: autoMaxBins(channel) };
    }
    else {
        return bin;
    }
}
const COMPATIBLE = { compatible: true };
function channelCompatibility(fieldDef, channel) {
    const type = fieldDef.type;
    if (type === 'geojson' && channel !== 'shape') {
        return {
            compatible: false,
            warning: `Channel ${channel} should not be used with a geojson data.`,
        };
    }
    switch (channel) {
        case ROW:
        case COLUMN:
        case FACET:
            if (!isDiscrete(fieldDef)) {
                return {
                    compatible: false,
                    warning: channelShouldBeDiscrete(channel),
                };
            }
            return COMPATIBLE;
        case X:
        case Y:
        case XOFFSET:
        case YOFFSET:
        case COLOR:
        case FILL:
        case STROKE:
        case TEXT$1:
        case DETAIL:
        case KEY:
        case TOOLTIP:
        case HREF:
        case URL$1:
        case ANGLE:
        case THETA:
        case RADIUS:
        case DESCRIPTION:
            return COMPATIBLE;
        case LONGITUDE:
        case LONGITUDE2:
        case LATITUDE:
        case LATITUDE2:
            if (type !== QUANTITATIVE) {
                return {
                    compatible: false,
                    warning: `Channel ${channel} should be used with a quantitative field only, not ${fieldDef.type} field.`,
                };
            }
            return COMPATIBLE;
        case OPACITY:
        case FILLOPACITY:
        case STROKEOPACITY:
        case STROKEWIDTH:
        case SIZE:
        case THETA2:
        case RADIUS2:
        case X2:
        case Y2:
        case TIME:
            if (type === 'nominal' && !fieldDef['sort']) {
                return {
                    compatible: false,
                    warning: `Channel ${channel} should not be used with an unsorted discrete field.`,
                };
            }
            return COMPATIBLE;
        case SHAPE:
        case STROKEDASH:
            if (!isDiscrete(fieldDef) && !isDiscretizing(fieldDef)) {
                return {
                    compatible: false,
                    warning: channelShouldBeDiscreteOrDiscretizing(channel),
                };
            }
            return COMPATIBLE;
        case ORDER:
            if (fieldDef.type === 'nominal' && !('sort' in fieldDef)) {
                return {
                    compatible: false,
                    warning: `Channel order is inappropriate for nominal field, which has no inherent order.`,
                };
            }
            return COMPATIBLE;
    }
}
/**
 * Check if the field def uses a time format or does not use any format but is temporal
 * (this does not cover field defs that are temporal but use a number format).
 */
function isFieldOrDatumDefForTimeFormat(fieldOrDatumDef) {
    const { formatType } = getFormatMixins(fieldOrDatumDef);
    return formatType === 'time' || (!formatType && isTemporalFieldDef(fieldOrDatumDef));
}
/**
 * Check if field def has type `temporal`. If you want to also cover field defs that use a time format, use `isFieldOrDatumDefForTimeFormat`.
 */
function isTemporalFieldDef(def) {
    return def && (def['type'] === 'temporal' || (isFieldDef(def) && !!def.timeUnit));
}
/**
 * Getting a value associated with a fielddef.
 * Convert the value to Vega expression if applicable (for datetime object, or string if the field def is temporal or has timeUnit)
 */
function valueExpr(v, { timeUnit, type, wrapTime, undefinedIfExprNotRequired, }) {
    const unit = timeUnit && normalizeTimeUnit(timeUnit)?.unit;
    let isTime = unit || type === 'temporal';
    let expr;
    if (isExprRef(v)) {
        expr = v.expr;
    }
    else if (isSignalRef(v)) {
        expr = v.signal;
    }
    else if (isDateTime(v)) {
        isTime = true;
        expr = dateTimeToExpr(v);
    }
    else if (isString(v) || isNumber$1(v)) {
        if (isTime) {
            expr = `datetime(${stringify$1(v)})`;
            if (isLocalSingleTimeUnit(unit)) {
                // for single timeUnit, we will use dateTimeToExpr to convert number/string to match the timeUnit
                if ((isNumber$1(v) && v < 10000) || (isString(v) && isNaN(Date.parse(v)))) {
                    expr = dateTimeToExpr({ [unit]: v });
                }
            }
        }
    }
    if (expr) {
        return wrapTime && isTime ? `time(${expr})` : expr;
    }
    // number or boolean or normal string
    return undefinedIfExprNotRequired ? undefined : stringify$1(v);
}
/**
 * Standardize value array -- convert each value to Vega expression if applicable
 */
function valueArray(fieldOrDatumDef, values) {
    const { type } = fieldOrDatumDef;
    return values.map((v) => {
        const timeUnit = isFieldDef(fieldOrDatumDef) && !isBinnedTimeUnit(fieldOrDatumDef.timeUnit) ? fieldOrDatumDef.timeUnit : undefined;
        const expr = valueExpr(v, {
            timeUnit,
            type,
            undefinedIfExprNotRequired: true,
        });
        // return signal for the expression if we need an expression
        if (expr !== undefined) {
            return { signal: expr };
        }
        // otherwise just return the original value
        return v;
    });
}
/**
 * Checks whether a fieldDef for a particular channel requires a computed bin range.
 */
function binRequiresRange(fieldDef, channel) {
    if (!isBinning(fieldDef.bin)) {
        console.warn('Only call this method for binned field defs.');
        return false;
    }
    // We need the range only when the user explicitly forces a binned field to be use discrete scale. In this case, bin range is used in axis and legend labels.
    // We could check whether the axis or legend exists (not disabled) but that seems overkill.
    return isScaleChannel(channel) && ['ordinal', 'nominal'].includes(fieldDef.type);
}

const CONDITIONAL_AXIS_PROP_INDEX = {
    labelAlign: {
        part: 'labels',
        vgProp: 'align',
    },
    labelBaseline: {
        part: 'labels',
        vgProp: 'baseline',
    },
    labelColor: {
        part: 'labels',
        vgProp: 'fill',
    },
    labelFont: {
        part: 'labels',
        vgProp: 'font',
    },
    labelFontSize: {
        part: 'labels',
        vgProp: 'fontSize',
    },
    labelFontStyle: {
        part: 'labels',
        vgProp: 'fontStyle',
    },
    labelFontWeight: {
        part: 'labels',
        vgProp: 'fontWeight',
    },
    labelOpacity: {
        part: 'labels',
        vgProp: 'opacity',
    },
    labelOffset: null,
    labelPadding: null, // There is no fixed vgProp for tickSize, need to use signal.
    gridColor: {
        part: 'grid',
        vgProp: 'stroke',
    },
    gridDash: {
        part: 'grid',
        vgProp: 'strokeDash',
    },
    gridDashOffset: {
        part: 'grid',
        vgProp: 'strokeDashOffset',
    },
    gridOpacity: {
        part: 'grid',
        vgProp: 'opacity',
    },
    gridWidth: {
        part: 'grid',
        vgProp: 'strokeWidth',
    },
    tickColor: {
        part: 'ticks',
        vgProp: 'stroke',
    },
    tickDash: {
        part: 'ticks',
        vgProp: 'strokeDash',
    },
    tickDashOffset: {
        part: 'ticks',
        vgProp: 'strokeDashOffset',
    },
    tickOpacity: {
        part: 'ticks',
        vgProp: 'opacity',
    },
    tickSize: null, // There is no fixed vgProp for tickSize, need to use signal.
    tickWidth: {
        part: 'ticks',
        vgProp: 'strokeWidth',
    },
};
function isConditionalAxisValue(v) {
    return v?.condition;
}
const AXIS_PARTS = ['domain', 'grid', 'labels', 'ticks', 'title'];
/**
 * A dictionary listing whether a certain axis property is applicable for only main axes or only grid axes.
 */
const AXIS_PROPERTY_TYPE = {
    grid: 'grid',
    gridCap: 'grid',
    gridColor: 'grid',
    gridDash: 'grid',
    gridDashOffset: 'grid',
    gridOpacity: 'grid',
    gridScale: 'grid',
    gridWidth: 'grid',
    orient: 'main',
    bandPosition: 'both', // Need to be applied to grid axis too, so the grid will align with ticks.
    aria: 'main',
    description: 'main',
    domain: 'main',
    domainCap: 'main',
    domainColor: 'main',
    domainDash: 'main',
    domainDashOffset: 'main',
    domainOpacity: 'main',
    domainWidth: 'main',
    format: 'main',
    formatType: 'main',
    labelAlign: 'main',
    labelAngle: 'main',
    labelBaseline: 'main',
    labelBound: 'main',
    labelColor: 'main',
    labelFlush: 'main',
    labelFlushOffset: 'main',
    labelFont: 'main',
    labelFontSize: 'main',
    labelFontStyle: 'main',
    labelFontWeight: 'main',
    labelLimit: 'main',
    labelLineHeight: 'main',
    labelOffset: 'main',
    labelOpacity: 'main',
    labelOverlap: 'main',
    labelPadding: 'main',
    labels: 'main',
    labelSeparation: 'main',
    maxExtent: 'main',
    minExtent: 'main',
    offset: 'both',
    position: 'main',
    tickCap: 'main',
    tickColor: 'main',
    tickDash: 'main',
    tickDashOffset: 'main',
    tickMinStep: 'both',
    tickOffset: 'both', // Need to be applied to grid axis too, so the grid will align with ticks.
    tickOpacity: 'main',
    tickRound: 'both', // Apply rounding to grid and ticks so they are aligned.
    ticks: 'main',
    tickSize: 'main',
    tickWidth: 'both',
    title: 'main',
    titleAlign: 'main',
    titleAnchor: 'main',
    titleAngle: 'main',
    titleBaseline: 'main',
    titleColor: 'main',
    titleFont: 'main',
    titleFontSize: 'main',
    titleFontStyle: 'main',
    titleFontWeight: 'main',
    titleLimit: 'main',
    titleLineHeight: 'main',
    titleOpacity: 'main',
    titlePadding: 'main',
    titleX: 'main',
    titleY: 'main',
    encode: 'both', // we hide this in Vega-Lite
    scale: 'both',
    tickBand: 'both',
    tickCount: 'both',
    tickExtra: 'both',
    translate: 'both',
    values: 'both',
    zindex: 'both', // this is actually set afterward, so it doesn't matter
};
const COMMON_AXIS_PROPERTIES_INDEX = {
    orient: 1, // other things can depend on orient
    aria: 1,
    bandPosition: 1,
    description: 1,
    domain: 1,
    domainCap: 1,
    domainColor: 1,
    domainDash: 1,
    domainDashOffset: 1,
    domainOpacity: 1,
    domainWidth: 1,
    format: 1,
    formatType: 1,
    grid: 1,
    gridCap: 1,
    gridColor: 1,
    gridDash: 1,
    gridDashOffset: 1,
    gridOpacity: 1,
    gridWidth: 1,
    labelAlign: 1,
    labelAngle: 1,
    labelBaseline: 1,
    labelBound: 1,
    labelColor: 1,
    labelFlush: 1,
    labelFlushOffset: 1,
    labelFont: 1,
    labelFontSize: 1,
    labelFontStyle: 1,
    labelFontWeight: 1,
    labelLimit: 1,
    labelLineHeight: 1,
    labelOffset: 1,
    labelOpacity: 1,
    labelOverlap: 1,
    labelPadding: 1,
    labels: 1,
    labelSeparation: 1,
    maxExtent: 1,
    minExtent: 1,
    offset: 1,
    position: 1,
    tickBand: 1,
    tickCap: 1,
    tickColor: 1,
    tickCount: 1,
    tickDash: 1,
    tickDashOffset: 1,
    tickExtra: 1,
    tickMinStep: 1,
    tickOffset: 1,
    tickOpacity: 1,
    tickRound: 1,
    ticks: 1,
    tickSize: 1,
    tickWidth: 1,
    title: 1,
    titleAlign: 1,
    titleAnchor: 1,
    titleAngle: 1,
    titleBaseline: 1,
    titleColor: 1,
    titleFont: 1,
    titleFontSize: 1,
    titleFontStyle: 1,
    titleFontWeight: 1,
    titleLimit: 1,
    titleLineHeight: 1,
    titleOpacity: 1,
    titlePadding: 1,
    titleX: 1,
    titleY: 1,
    translate: 1,
    values: 1,
    zindex: 1,
};
const AXIS_PROPERTIES_INDEX = {
    ...COMMON_AXIS_PROPERTIES_INDEX,
    style: 1,
    labelExpr: 1,
    encoding: 1,
};
function isAxisProperty(prop) {
    return has$1(AXIS_PROPERTIES_INDEX, prop);
}
const AXIS_CONFIGS_INDEX = {
    axis: 1,
    axisBand: 1,
    axisBottom: 1,
    axisDiscrete: 1,
    axisLeft: 1,
    axisPoint: 1,
    axisQuantitative: 1,
    axisRight: 1,
    axisTemporal: 1,
    axisTop: 1,
    axisX: 1,
    axisXBand: 1,
    axisXDiscrete: 1,
    axisXPoint: 1,
    axisXQuantitative: 1,
    axisXTemporal: 1,
    axisY: 1,
    axisYBand: 1,
    axisYDiscrete: 1,
    axisYPoint: 1,
    axisYQuantitative: 1,
    axisYTemporal: 1,
};
const AXIS_CONFIGS = keys(AXIS_CONFIGS_INDEX);

function isUnitSpec(spec) {
    return hasProperty(spec, 'mark');
}

class CompositeMarkNormalizer {
    name;
    run;
    constructor(name, run) {
        this.name = name;
        this.run = run;
    }
    hasMatchingType(spec) {
        if (isUnitSpec(spec)) {
            return getMarkType(spec.mark) === this.name;
        }
        return false;
    }
}

function channelHasField(encoding, channel) {
    const channelDef = encoding?.[channel];
    if (channelDef) {
        if (isArray(channelDef)) {
            return some(channelDef, (fieldDef) => !!fieldDef.field);
        }
        else {
            return isFieldDef(channelDef) || hasConditionalFieldDef(channelDef);
        }
    }
    return false;
}
function channelHasFieldOrDatum(encoding, channel) {
    const channelDef = encoding?.[channel];
    if (channelDef) {
        if (isArray(channelDef)) {
            return some(channelDef, (fieldDef) => !!fieldDef.field);
        }
        else {
            return isFieldDef(channelDef) || isDatumDef(channelDef) || hasConditionalFieldOrDatumDef(channelDef);
        }
    }
    return false;
}
function channelHasNestedOffsetScale(encoding, channel) {
    if (isXorY(channel)) {
        const fieldDef = encoding[channel];
        if ((isFieldDef(fieldDef) || isDatumDef(fieldDef)) &&
            (isDiscrete$1(fieldDef.type) || (isFieldDef(fieldDef) && fieldDef.timeUnit))) {
            const offsetChannel = getOffsetScaleChannel(channel);
            return channelHasFieldOrDatum(encoding, offsetChannel);
        }
    }
    return false;
}
function isAggregate$1(encoding) {
    return some(CHANNELS, (channel) => {
        if (channelHasField(encoding, channel)) {
            const channelDef = encoding[channel];
            if (isArray(channelDef)) {
                return some(channelDef, (fieldDef) => !!fieldDef.aggregate);
            }
            else {
                const fieldDef = getFieldDef(channelDef);
                return fieldDef && !!fieldDef.aggregate;
            }
        }
        return false;
    });
}
function extractTransformsFromEncoding(oldEncoding, config) {
    const groupby = [];
    const bins = [];
    const timeUnits = [];
    const aggregate = [];
    const encoding = {};
    forEach(oldEncoding, (channelDef, channel) => {
        // Extract potential embedded transformations along with remaining properties
        if (isFieldDef(channelDef)) {
            const { field, aggregate: aggOp, bin, timeUnit, ...remaining } = channelDef;
            if (aggOp || timeUnit || bin) {
                const guide = getGuide(channelDef);
                const isTitleDefined = guide?.title;
                let newField = vgField(channelDef, { forAs: true });
                const newFieldDef = {
                    // Only add title if it doesn't exist
                    ...(isTitleDefined ? [] : { title: title(channelDef, config, { allowDisabling: true }) }),
                    ...remaining,
                    // Always overwrite field
                    field: newField,
                };
                if (aggOp) {
                    let op;
                    if (isArgmaxDef(aggOp)) {
                        op = 'argmax';
                        newField = vgField({ op: 'argmax', field: aggOp.argmax }, { forAs: true });
                        newFieldDef.field = `${newField}.${field}`;
                    }
                    else if (isArgminDef(aggOp)) {
                        op = 'argmin';
                        newField = vgField({ op: 'argmin', field: aggOp.argmin }, { forAs: true });
                        newFieldDef.field = `${newField}.${field}`;
                    }
                    else if (aggOp !== 'boxplot' && aggOp !== 'errorbar' && aggOp !== 'errorband') {
                        op = aggOp;
                    }
                    if (op) {
                        const aggregateEntry = {
                            op,
                            as: newField,
                        };
                        if (field) {
                            aggregateEntry.field = field;
                        }
                        aggregate.push(aggregateEntry);
                    }
                }
                else {
                    groupby.push(newField);
                    if (isTypedFieldDef(channelDef) && isBinning(bin)) {
                        bins.push({ bin, field, as: newField });
                        // Add additional groupbys for range and end of bins
                        groupby.push(vgField(channelDef, { binSuffix: 'end' }));
                        if (binRequiresRange(channelDef, channel)) {
                            groupby.push(vgField(channelDef, { binSuffix: 'range' }));
                        }
                        // Create accompanying 'x2' or 'y2' field if channel is 'x' or 'y' respectively
                        if (isXorY(channel)) {
                            const secondaryChannel = {
                                field: `${newField}_end`,
                            };
                            encoding[`${channel}2`] = secondaryChannel;
                        }
                        newFieldDef.bin = 'binned';
                        if (!isSecondaryRangeChannel(channel)) {
                            newFieldDef['type'] = QUANTITATIVE;
                        }
                    }
                    else if (timeUnit && !isBinnedTimeUnit(timeUnit)) {
                        timeUnits.push({
                            timeUnit,
                            field,
                            as: newField,
                        });
                        // define the format type for later compilation
                        const formatType = isTypedFieldDef(channelDef) && channelDef.type !== TEMPORAL && 'time';
                        if (formatType) {
                            if (channel === TEXT$1 || channel === TOOLTIP) {
                                newFieldDef['formatType'] = formatType;
                            }
                            else if (isNonPositionScaleChannel(channel)) {
                                newFieldDef['legend'] = {
                                    formatType,
                                    ...newFieldDef['legend'],
                                };
                            }
                            else if (isXorY(channel)) {
                                newFieldDef['axis'] = {
                                    formatType,
                                    ...newFieldDef['axis'],
                                };
                            }
                        }
                    }
                }
                // now the field should refer to post-transformed field instead
                encoding[channel] = newFieldDef;
            }
            else {
                groupby.push(field);
                encoding[channel] = oldEncoding[channel];
            }
        }
        else {
            // For value def / signal ref / datum def, just copy
            encoding[channel] = oldEncoding[channel];
        }
    });
    return {
        bins,
        timeUnits,
        aggregate,
        groupby,
        encoding,
    };
}
function markChannelCompatible(encoding, channel, mark) {
    const markSupported = supportMark(channel, mark);
    if (!markSupported) {
        return false;
    }
    else if (markSupported === 'binned') {
        const primaryFieldDef = encoding[channel === X2 ? X : Y];
        // circle, point, square and tick only support x2/y2 when their corresponding x/y fieldDef
        // has "binned" data and thus need x2/y2 to specify the bin-end field.
        if (isFieldDef(primaryFieldDef) && isFieldDef(encoding[channel]) && isBinned(primaryFieldDef.bin)) {
            return true;
        }
        else {
            return false;
        }
    }
    return true;
}
function initEncoding(encoding, mark, filled, config) {
    const normalizedEncoding = {};
    for (const key of keys(encoding)) {
        if (!isChannel(key)) {
            // Drop invalid channel
            warn(invalidEncodingChannel(key));
        }
    }
    for (let channel of UNIT_CHANNELS) {
        if (!encoding[channel]) {
            continue;
        }
        const channelDef = encoding[channel];
        if (isXorYOffset(channel)) {
            const mainChannel = getMainChannelFromOffsetChannel(channel);
            const positionDef = normalizedEncoding[mainChannel];
            if (isFieldDef(positionDef)) {
                if (isContinuous(positionDef.type)) {
                    if (isFieldDef(channelDef) && !positionDef.timeUnit) {
                        // TODO: nesting continuous field instead continuous field should
                        // behave like offsetting the data in data domain
                        warn(offsetNestedInsideContinuousPositionScaleDropped(mainChannel));
                        continue;
                    }
                }
            }
        }
        if (channel === 'angle' && mark === 'arc' && !encoding.theta) {
            warn(REPLACE_ANGLE_WITH_THETA);
            channel = THETA;
        }
        if (!markChannelCompatible(encoding, channel, mark)) {
            // Drop unsupported channel
            warn(incompatibleChannel(channel, mark));
            continue;
        }
        // Drop line's size if the field is aggregated.
        if (channel === SIZE && mark === 'line') {
            const fieldDef = getFieldDef(encoding[channel]);
            if (fieldDef?.aggregate) {
                warn(LINE_WITH_VARYING_SIZE);
                continue;
            }
        }
        // Drop color if either fill or stroke is specified
        if (channel === COLOR && (filled ? 'fill' in encoding : 'stroke' in encoding)) {
            warn(droppingColor('encoding', { fill: 'fill' in encoding, stroke: 'stroke' in encoding }));
            continue;
        }
        if (channel === DETAIL ||
            (channel === ORDER && !isArray(channelDef) && !isValueDef(channelDef)) ||
            (channel === TOOLTIP && isArray(channelDef))) {
            if (channelDef) {
                if (channel === ORDER) {
                    const def = encoding[channel];
                    if (isOrderOnlyDef(def)) {
                        normalizedEncoding[channel] = def;
                        continue;
                    }
                }
                // Array of fieldDefs for detail channel (or production rule)
                normalizedEncoding[channel] = array$2(channelDef).reduce((defs, fieldDef) => {
                    if (!isFieldDef(fieldDef)) {
                        warn(emptyFieldDef(fieldDef, channel));
                    }
                    else {
                        defs.push(initFieldDef(fieldDef, channel));
                    }
                    return defs;
                }, []);
            }
        }
        else {
            if (channel === TOOLTIP && channelDef === null) {
                // Preserve null so we can use it to disable tooltip
                normalizedEncoding[channel] = null;
            }
            else if (!isFieldDef(channelDef) &&
                !isDatumDef(channelDef) &&
                !isValueDef(channelDef) &&
                !isConditionalDef(channelDef) &&
                !isSignalRef(channelDef)) {
                warn(emptyFieldDef(channelDef, channel));
                continue;
            }
            normalizedEncoding[channel] = initChannelDef(channelDef, channel, config);
        }
    }
    return normalizedEncoding;
}
/**
 * For composite marks, we have to call initChannelDef during init so we can infer types earlier.
 */
function normalizeEncoding(encoding, config) {
    const normalizedEncoding = {};
    for (const channel of keys(encoding)) {
        const newChannelDef = initChannelDef(encoding[channel], channel, config, { compositeMark: true });
        normalizedEncoding[channel] = newChannelDef;
    }
    return normalizedEncoding;
}
function fieldDefs(encoding) {
    const arr = [];
    for (const channel of keys(encoding)) {
        if (channelHasField(encoding, channel)) {
            const channelDef = encoding[channel];
            const channelDefArray = array$2(channelDef);
            for (const def of channelDefArray) {
                if (isFieldDef(def)) {
                    arr.push(def);
                }
                else if (hasConditionalFieldDef(def)) {
                    arr.push(def.condition);
                }
            }
        }
    }
    return arr;
}
function forEach(mapping, f, thisArg) {
    if (!mapping) {
        return;
    }
    for (const channel of keys(mapping)) {
        const el = mapping[channel];
        if (isArray(el)) {
            for (const channelDef of el) {
                f.call(thisArg, channelDef, channel);
            }
        }
        else {
            f.call(thisArg, el, channel);
        }
    }
}
function reduce(mapping, f, init, thisArg) {
    if (!mapping) {
        return init;
    }
    return keys(mapping).reduce((r, channel) => {
        const map = mapping[channel];
        if (isArray(map)) {
            return map.reduce((r1, channelDef) => {
                return f.call(thisArg, r1, channelDef, channel);
            }, r);
        }
        else {
            return f.call(thisArg, r, map, channel);
        }
    }, init);
}
/**
 * Returns list of path grouping fields for the given encoding
 */
function pathGroupingFields(mark, encoding) {
    return keys(encoding).reduce((details, channel) => {
        switch (channel) {
            // x, y, x2, y2, lat, long, lat1, long2, order, tooltip, href, aria label, cursor should not cause lines to group
            case X:
            case Y:
            case HREF:
            case DESCRIPTION:
            case URL$1:
            case X2:
            case Y2:
            case XOFFSET:
            case YOFFSET:
            case THETA:
            case THETA2:
            case RADIUS:
            case RADIUS2:
            case TIME:
            // falls through
            case LATITUDE:
            case LONGITUDE:
            case LATITUDE2:
            case LONGITUDE2:
            // TODO: case 'cursor':
            // text, shape, shouldn't be a part of line/trail/area [falls through]
            case TEXT$1:
            case SHAPE:
            case ANGLE:
            // falls through
            // tooltip fields should not be added to group by [falls through]
            case TOOLTIP:
                return details;
            case ORDER:
                // order should not group line / trail
                if (mark === 'line' || mark === 'trail') {
                    return details;
                }
            // but order should group area for stacking (falls through)
            case DETAIL:
            case KEY: {
                const channelDef = encoding[channel];
                if (isArray(channelDef) || isFieldDef(channelDef)) {
                    for (const fieldDef of array$2(channelDef)) {
                        if (!fieldDef.aggregate) {
                            details.push(vgField(fieldDef, {}));
                        }
                    }
                }
                return details;
            }
            case SIZE:
                if (mark === 'trail') {
                    // For trail, size should not group trail lines.
                    return details;
                }
            // For line, size should group lines.
            // falls through
            case COLOR:
            case FILL:
            case STROKE:
            case OPACITY:
            case FILLOPACITY:
            case STROKEOPACITY:
            case STROKEDASH:
            case STROKEWIDTH: {
                // TODO strokeDashOffset:
                // falls through
                const fieldDef = getFieldDef(encoding[channel]);
                if (fieldDef && !fieldDef.aggregate) {
                    details.push(vgField(fieldDef, {}));
                }
                return details;
            }
        }
    }, []);
}

function filterTooltipWithAggregatedField(oldEncoding) {
    const { tooltip, ...filteredEncoding } = oldEncoding;
    if (!tooltip) {
        return { filteredEncoding };
    }
    let customTooltipWithAggregatedField;
    let customTooltipWithoutAggregatedField;
    if (isArray(tooltip)) {
        for (const t of tooltip) {
            if (t.aggregate) {
                if (!customTooltipWithAggregatedField) {
                    customTooltipWithAggregatedField = [];
                }
                customTooltipWithAggregatedField.push(t);
            }
            else {
                if (!customTooltipWithoutAggregatedField) {
                    customTooltipWithoutAggregatedField = [];
                }
                customTooltipWithoutAggregatedField.push(t);
            }
        }
        if (customTooltipWithAggregatedField) {
            filteredEncoding.tooltip = customTooltipWithAggregatedField;
        }
    }
    else {
        if (tooltip.aggregate) {
            filteredEncoding.tooltip = tooltip;
        }
        else {
            customTooltipWithoutAggregatedField = tooltip;
        }
    }
    if (isArray(customTooltipWithoutAggregatedField) && customTooltipWithoutAggregatedField.length === 1) {
        customTooltipWithoutAggregatedField = customTooltipWithoutAggregatedField[0];
    }
    return { customTooltipWithoutAggregatedField, filteredEncoding };
}
function getCompositeMarkTooltip(tooltipSummary, continuousAxisChannelDef, encodingWithoutContinuousAxis, withFieldName = true) {
    if ('tooltip' in encodingWithoutContinuousAxis) {
        return { tooltip: encodingWithoutContinuousAxis.tooltip };
    }
    const fiveSummaryTooltip = tooltipSummary.map(({ fieldPrefix, titlePrefix }) => {
        const mainTitle = withFieldName ? ` of ${getTitle(continuousAxisChannelDef)}` : '';
        return {
            field: fieldPrefix + continuousAxisChannelDef.field,
            type: continuousAxisChannelDef.type,
            title: isSignalRef(titlePrefix) ? { signal: `${titlePrefix}"${escape(mainTitle)}"` } : titlePrefix + mainTitle,
        };
    });
    const tooltipFieldDefs = fieldDefs(encodingWithoutContinuousAxis).map(toStringFieldDef);
    return {
        tooltip: [
            ...fiveSummaryTooltip,
            // need to cast because TextFieldDef supports fewer types of bin
            ...unique(tooltipFieldDefs, hash),
        ],
    };
}
function getTitle(continuousAxisChannelDef) {
    const { title, field } = continuousAxisChannelDef;
    return getFirstDefined(title, field);
}
function makeCompositeAggregatePartFactory(compositeMarkDef, continuousAxis, continuousAxisChannelDef, sharedEncoding, compositeMarkConfig) {
    const { scale, axis } = continuousAxisChannelDef;
    return ({ partName, mark, positionPrefix, endPositionPrefix = undefined, extraEncoding = {}, }) => {
        const title = getTitle(continuousAxisChannelDef);
        return partLayerMixins(compositeMarkDef, partName, compositeMarkConfig, {
            mark, // TODO better remove this method and just have mark as a parameter of the method
            encoding: {
                [continuousAxis]: {
                    field: `${positionPrefix}_${continuousAxisChannelDef.field}`,
                    type: continuousAxisChannelDef.type,
                    ...(title !== undefined ? { title } : {}),
                    ...(scale !== undefined ? { scale } : {}),
                    ...(axis !== undefined ? { axis } : {}),
                },
                ...(isString(endPositionPrefix)
                    ? {
                        [`${continuousAxis}2`]: {
                            field: `${endPositionPrefix}_${continuousAxisChannelDef.field}`,
                        },
                    }
                    : {}),
                ...sharedEncoding,
                ...extraEncoding,
            },
        });
    };
}
function partLayerMixins(markDef, part, compositeMarkConfig, partBaseSpec) {
    const { clip, color, opacity } = markDef;
    const mark = markDef.type;
    if (markDef[part] || (markDef[part] === undefined && compositeMarkConfig[part])) {
        return [
            {
                ...partBaseSpec,
                mark: {
                    ...compositeMarkConfig[part],
                    ...(clip ? { clip } : {}),
                    ...(color ? { color } : {}),
                    ...(opacity ? { opacity } : {}),
                    ...(isMarkDef(partBaseSpec.mark) ? partBaseSpec.mark : { type: partBaseSpec.mark }),
                    style: `${mark}-${String(part)}`,
                    ...(isBoolean$2(markDef[part]) ? {} : markDef[part]),
                },
            },
        ];
    }
    return [];
}
function compositeMarkContinuousAxis(spec, orient, compositeMark) {
    const { encoding } = spec;
    const continuousAxis = orient === 'vertical' ? 'y' : 'x';
    const continuousAxisChannelDef = encoding[continuousAxis]; // Safe to cast because if x is not continuous fielddef, the orient would not be horizontal.
    const continuousAxisChannelDef2 = encoding[`${continuousAxis}2`];
    const continuousAxisChannelDefError = encoding[`${continuousAxis}Error`];
    const continuousAxisChannelDefError2 = encoding[`${continuousAxis}Error2`];
    return {
        continuousAxisChannelDef: filterAggregateFromChannelDef(continuousAxisChannelDef, compositeMark),
        continuousAxisChannelDef2: filterAggregateFromChannelDef(continuousAxisChannelDef2, compositeMark),
        continuousAxisChannelDefError: filterAggregateFromChannelDef(continuousAxisChannelDefError, compositeMark),
        continuousAxisChannelDefError2: filterAggregateFromChannelDef(continuousAxisChannelDefError2, compositeMark),
        continuousAxis,
    };
}
function filterAggregateFromChannelDef(continuousAxisChannelDef, compositeMark) {
    if (continuousAxisChannelDef?.aggregate) {
        const { aggregate, ...continuousAxisWithoutAggregate } = continuousAxisChannelDef;
        if (aggregate !== compositeMark) {
            warn(errorBarContinuousAxisHasCustomizedAggregate(aggregate, compositeMark));
        }
        return continuousAxisWithoutAggregate;
    }
    else {
        return continuousAxisChannelDef;
    }
}
function compositeMarkOrient(spec, compositeMark) {
    const { mark, encoding } = spec;
    const { x, y } = encoding;
    if (isMarkDef(mark) && mark.orient) {
        return mark.orient;
    }
    if (isContinuousFieldOrDatumDef(x)) {
        // x is continuous
        if (isContinuousFieldOrDatumDef(y)) {
            // both x and y are continuous
            const xAggregate = isFieldDef(x) && x.aggregate;
            const yAggregate = isFieldDef(y) && y.aggregate;
            if (!xAggregate && yAggregate === compositeMark) {
                return 'vertical';
            }
            else if (!yAggregate && xAggregate === compositeMark) {
                return 'horizontal';
            }
            else if (xAggregate === compositeMark && yAggregate === compositeMark) {
                throw new Error('Both x and y cannot have aggregate');
            }
            else {
                if (isFieldOrDatumDefForTimeFormat(y) && !isFieldOrDatumDefForTimeFormat(x)) {
                    // y is temporal but x is not
                    return 'horizontal';
                }
                // default orientation for two continuous
                return 'vertical';
            }
        }
        return 'horizontal';
    }
    else if (isContinuousFieldOrDatumDef(y)) {
        // y is continuous but x is not
        return 'vertical';
    }
    else {
        // Neither x nor y is continuous.
        throw new Error(`Need a valid continuous axis for ${compositeMark}s`);
    }
}

const BOXPLOT = 'boxplot';
const BOXPLOT_PARTS = ['box', 'median', 'outliers', 'rule', 'ticks'];
const boxPlotNormalizer = new CompositeMarkNormalizer(BOXPLOT, normalizeBoxPlot);
function getBoxPlotType(extent) {
    if (isNumber$1(extent)) {
        return 'tukey';
    }
    // Ham: If we ever want to, we could add another extent syntax `{kIQR: number}` for the original [Q1-k*IQR, Q3+k*IQR] whisker and call this boxPlotType = `kIQR`. However, I'm not exposing this for now.
    return extent;
}
function normalizeBoxPlot(spec, { config }) {
    // Need to initEncoding first so we can infer type
    spec = {
        ...spec,
        encoding: normalizeEncoding(spec.encoding, config),
    };
    const { mark, encoding: _encoding, params, projection: _p, ...outerSpec } = spec;
    const markDef = isMarkDef(mark) ? mark : { type: mark };
    // TODO(https://github.com/vega/vega-lite/issues/3702): add selection support
    if (params) {
        warn(selectionNotSupported('boxplot'));
    }
    const extent = markDef.extent ?? config.boxplot.extent;
    const sizeValue = getMarkPropOrConfig('size', markDef, // TODO: https://github.com/vega/vega-lite/issues/6245
    config);
    const invalid = markDef.invalid;
    const boxPlotType = getBoxPlotType(extent);
    const { bins, timeUnits, transform, continuousAxisChannelDef, continuousAxis, groupby, aggregate, encodingWithoutContinuousAxis, ticksOrient, boxOrient, customTooltipWithoutAggregatedField, } = boxParams(spec, extent, config);
    const aliasedFieldName = removePathFromField(continuousAxisChannelDef.field);
    const { color, size, ...encodingWithoutSizeColorAndContinuousAxis } = encodingWithoutContinuousAxis;
    const makeBoxPlotPart = (sharedEncoding) => {
        return makeCompositeAggregatePartFactory(markDef, continuousAxis, continuousAxisChannelDef, sharedEncoding, config.boxplot);
    };
    const makeBoxPlotExtent = makeBoxPlotPart(encodingWithoutSizeColorAndContinuousAxis);
    const makeBoxPlotBox = makeBoxPlotPart(encodingWithoutContinuousAxis);
    const defaultBoxColor = (isObject(config.boxplot.box) ? config.boxplot.box.color : config.mark.color) || '#4c78a8';
    const makeBoxPlotMidTick = makeBoxPlotPart({
        ...encodingWithoutSizeColorAndContinuousAxis,
        ...(size ? { size } : {}),
        color: {
            condition: {
                test: `${accessWithDatumToUnescapedPath(`lower_box_${continuousAxisChannelDef.field}`)} >= ${accessWithDatumToUnescapedPath(`upper_box_${continuousAxisChannelDef.field}`)}`,
                ...(color || { value: defaultBoxColor }),
            },
        },
    });
    const fiveSummaryTooltipEncoding = getCompositeMarkTooltip([
        { fieldPrefix: boxPlotType === 'min-max' ? 'upper_whisker_' : 'max_', titlePrefix: 'Max' },
        { fieldPrefix: 'upper_box_', titlePrefix: 'Q3' },
        { fieldPrefix: 'mid_box_', titlePrefix: 'Median' },
        { fieldPrefix: 'lower_box_', titlePrefix: 'Q1' },
        { fieldPrefix: boxPlotType === 'min-max' ? 'lower_whisker_' : 'min_', titlePrefix: 'Min' },
    ], continuousAxisChannelDef, encodingWithoutContinuousAxis);
    // ## Whisker Layers
    const endTick = { type: 'tick', color: 'black', opacity: 1, orient: ticksOrient, invalid, aria: false };
    const whiskerTooltipEncoding = boxPlotType === 'min-max'
        ? fiveSummaryTooltipEncoding // for min-max, show five-summary tooltip for whisker
        : // for tukey / k-IQR, just show upper/lower-whisker
            getCompositeMarkTooltip([
                { fieldPrefix: 'upper_whisker_', titlePrefix: 'Upper Whisker' },
                { fieldPrefix: 'lower_whisker_', titlePrefix: 'Lower Whisker' },
            ], continuousAxisChannelDef, encodingWithoutContinuousAxis);
    const whiskerLayers = [
        ...makeBoxPlotExtent({
            partName: 'rule',
            mark: { type: 'rule', invalid, aria: false },
            positionPrefix: 'lower_whisker',
            endPositionPrefix: 'lower_box',
            extraEncoding: whiskerTooltipEncoding,
        }),
        ...makeBoxPlotExtent({
            partName: 'rule',
            mark: { type: 'rule', invalid, aria: false },
            positionPrefix: 'upper_box',
            endPositionPrefix: 'upper_whisker',
            extraEncoding: whiskerTooltipEncoding,
        }),
        ...makeBoxPlotExtent({
            partName: 'ticks',
            mark: endTick,
            positionPrefix: 'lower_whisker',
            extraEncoding: whiskerTooltipEncoding,
        }),
        ...makeBoxPlotExtent({
            partName: 'ticks',
            mark: endTick,
            positionPrefix: 'upper_whisker',
            extraEncoding: whiskerTooltipEncoding,
        }),
    ];
    // ## Box Layers
    // TODO: support hiding certain mark parts
    const boxLayers = [
        ...(boxPlotType !== 'tukey' ? whiskerLayers : []),
        ...makeBoxPlotBox({
            partName: 'box',
            mark: {
                type: 'bar',
                ...(sizeValue ? { size: sizeValue } : {}),
                orient: boxOrient,
                invalid,
                ariaRoleDescription: 'box',
            },
            positionPrefix: 'lower_box',
            endPositionPrefix: 'upper_box',
            extraEncoding: fiveSummaryTooltipEncoding,
        }),
        ...makeBoxPlotMidTick({
            partName: 'median',
            mark: {
                type: 'tick',
                invalid,
                ...(isObject(config.boxplot.median) && config.boxplot.median.color ? { color: config.boxplot.median.color } : {}),
                ...(sizeValue ? { size: sizeValue } : {}),
                orient: ticksOrient,
                aria: false,
            },
            positionPrefix: 'mid_box',
            extraEncoding: fiveSummaryTooltipEncoding,
        }),
    ];
    if (boxPlotType === 'min-max') {
        return {
            ...outerSpec,
            transform: (outerSpec.transform ?? []).concat(transform),
            layer: boxLayers,
        };
    }
    // Tukey Box Plot
    const lowerBoxExpr = accessWithDatumToUnescapedPath(`lower_box_${continuousAxisChannelDef.field}`);
    const upperBoxExpr = accessWithDatumToUnescapedPath(`upper_box_${continuousAxisChannelDef.field}`);
    const iqrExpr = `(${upperBoxExpr} - ${lowerBoxExpr})`;
    const lowerWhiskerExpr = `${lowerBoxExpr} - ${extent} * ${iqrExpr}`;
    const upperWhiskerExpr = `${upperBoxExpr} + ${extent} * ${iqrExpr}`;
    const fieldExpr = accessWithDatumToUnescapedPath(continuousAxisChannelDef.field);
    const joinaggregateTransform = {
        joinaggregate: boxParamsQuartiles(continuousAxisChannelDef.field),
        groupby,
    };
    const filteredWhiskerSpec = {
        transform: [
            {
                filter: `(${lowerWhiskerExpr} <= ${fieldExpr}) && (${fieldExpr} <= ${upperWhiskerExpr})`,
            },
            {
                aggregate: [
                    {
                        op: 'min',
                        field: continuousAxisChannelDef.field,
                        as: `lower_whisker_${aliasedFieldName}`,
                    },
                    {
                        op: 'max',
                        field: continuousAxisChannelDef.field,
                        as: `upper_whisker_${aliasedFieldName}`,
                    },
                    // preserve lower_box / upper_box
                    {
                        op: 'min',
                        field: `lower_box_${continuousAxisChannelDef.field}`,
                        as: `lower_box_${aliasedFieldName}`,
                    },
                    {
                        op: 'max',
                        field: `upper_box_${continuousAxisChannelDef.field}`,
                        as: `upper_box_${aliasedFieldName}`,
                    },
                    ...aggregate,
                ],
                groupby,
            },
        ],
        layer: whiskerLayers,
    };
    const { tooltip, ...encodingWithoutSizeColorContinuousAxisAndTooltip } = encodingWithoutSizeColorAndContinuousAxis;
    const { scale, axis } = continuousAxisChannelDef;
    const title = getTitle(continuousAxisChannelDef);
    const outlierLayersMixins = partLayerMixins(markDef, 'outliers', config.boxplot, {
        transform: [{ filter: `(${fieldExpr} < ${lowerWhiskerExpr}) || (${fieldExpr} > ${upperWhiskerExpr})` }],
        mark: 'point',
        encoding: {
            [continuousAxis]: {
                field: continuousAxisChannelDef.field,
                type: continuousAxisChannelDef.type,
                ...(title !== undefined ? { title } : {}),
                ...(scale !== undefined ? { scale } : {}),
                ...(axis !== undefined ? { axis } : {}),
            },
            ...encodingWithoutSizeColorContinuousAxisAndTooltip,
            ...(color ? { color } : {}),
            ...(customTooltipWithoutAggregatedField ? { tooltip: customTooltipWithoutAggregatedField } : {}),
        },
    })[0];
    let filteredLayersMixins;
    const filteredLayersMixinsTransforms = [...bins, ...timeUnits, joinaggregateTransform];
    if (outlierLayersMixins) {
        filteredLayersMixins = {
            transform: filteredLayersMixinsTransforms,
            layer: [outlierLayersMixins, filteredWhiskerSpec],
        };
    }
    else {
        filteredLayersMixins = filteredWhiskerSpec;
        filteredLayersMixins.transform.unshift(...filteredLayersMixinsTransforms);
    }
    return {
        ...outerSpec,
        layer: [
            filteredLayersMixins,
            {
                // boxplot
                transform,
                layer: boxLayers,
            },
        ],
    };
}
function boxParamsQuartiles(continousAxisField) {
    const aliasedFieldName = removePathFromField(continousAxisField);
    return [
        {
            op: 'q1',
            field: continousAxisField,
            as: `lower_box_${aliasedFieldName}`,
        },
        {
            op: 'q3',
            field: continousAxisField,
            as: `upper_box_${aliasedFieldName}`,
        },
    ];
}
function boxParams(spec, extent, config) {
    const orient = compositeMarkOrient(spec, BOXPLOT);
    const { continuousAxisChannelDef, continuousAxis } = compositeMarkContinuousAxis(spec, orient, BOXPLOT);
    const continuousFieldName = continuousAxisChannelDef.field;
    const aliasedFieldName = removePathFromField(continuousFieldName);
    const boxPlotType = getBoxPlotType(extent);
    const boxplotSpecificAggregate = [
        ...boxParamsQuartiles(continuousFieldName),
        {
            op: 'median',
            field: continuousFieldName,
            as: `mid_box_${aliasedFieldName}`,
        },
        {
            op: 'min',
            field: continuousFieldName,
            as: (boxPlotType === 'min-max' ? 'lower_whisker_' : 'min_') + aliasedFieldName,
        },
        {
            op: 'max',
            field: continuousFieldName,
            as: (boxPlotType === 'min-max' ? 'upper_whisker_' : 'max_') + aliasedFieldName,
        },
    ];
    const postAggregateCalculates = boxPlotType === 'min-max' || boxPlotType === 'tukey'
        ? []
        : [
            // This is for the  original k-IQR, which we do not expose
            {
                calculate: `${accessWithDatumToUnescapedPath(`upper_box_${aliasedFieldName}`)} - ${accessWithDatumToUnescapedPath(`lower_box_${aliasedFieldName}`)}`,
                as: `iqr_${aliasedFieldName}`,
            },
            {
                calculate: `min(${accessWithDatumToUnescapedPath(`upper_box_${aliasedFieldName}`)} + ${accessWithDatumToUnescapedPath(`iqr_${aliasedFieldName}`)} * ${extent}, ${accessWithDatumToUnescapedPath(`max_${aliasedFieldName}`)})`,
                as: `upper_whisker_${aliasedFieldName}`,
            },
            {
                calculate: `max(${accessWithDatumToUnescapedPath(`lower_box_${aliasedFieldName}`)} - ${accessWithDatumToUnescapedPath(`iqr_${aliasedFieldName}`)} * ${extent}, ${accessWithDatumToUnescapedPath(`min_${aliasedFieldName}`)})`,
                as: `lower_whisker_${aliasedFieldName}`,
            },
        ];
    const { [continuousAxis]: oldContinuousAxisChannelDef, ...oldEncodingWithoutContinuousAxis } = spec.encoding;
    const { customTooltipWithoutAggregatedField, filteredEncoding } = filterTooltipWithAggregatedField(oldEncodingWithoutContinuousAxis);
    const { bins, timeUnits, aggregate, groupby, encoding: encodingWithoutContinuousAxis, } = extractTransformsFromEncoding(filteredEncoding, config);
    const ticksOrient = orient === 'vertical' ? 'horizontal' : 'vertical';
    const boxOrient = orient;
    const transform = [
        ...bins,
        ...timeUnits,
        {
            aggregate: [...aggregate, ...boxplotSpecificAggregate],
            groupby,
        },
        ...postAggregateCalculates,
    ];
    return {
        bins,
        timeUnits,
        transform,
        groupby,
        aggregate,
        continuousAxisChannelDef,
        continuousAxis,
        encodingWithoutContinuousAxis,
        ticksOrient,
        boxOrient,
        customTooltipWithoutAggregatedField,
    };
}

const ERRORBAR = 'errorbar';
const ERRORBAR_PARTS = ['ticks', 'rule'];
const errorBarNormalizer = new CompositeMarkNormalizer(ERRORBAR, normalizeErrorBar);
function normalizeErrorBar(spec, { config }) {
    // Need to initEncoding first so we can infer type
    spec = {
        ...spec,
        encoding: normalizeEncoding(spec.encoding, config),
    };
    const { transform, continuousAxisChannelDef, continuousAxis, encodingWithoutContinuousAxis, ticksOrient, markDef, outerSpec, tooltipEncoding, } = errorBarParams(spec, ERRORBAR, config);
    delete encodingWithoutContinuousAxis.size;
    const makeErrorBarPart = makeCompositeAggregatePartFactory(markDef, continuousAxis, continuousAxisChannelDef, encodingWithoutContinuousAxis, config.errorbar);
    const thickness = markDef.thickness;
    const size = markDef.size;
    const tick = {
        type: 'tick',
        orient: ticksOrient,
        aria: false,
        ...(thickness !== undefined ? { thickness } : {}),
        ...(size !== undefined ? { size } : {}),
    };
    const layer = [
        ...makeErrorBarPart({
            partName: 'ticks',
            mark: tick,
            positionPrefix: 'lower',
            extraEncoding: tooltipEncoding,
        }),
        ...makeErrorBarPart({
            partName: 'ticks',
            mark: tick,
            positionPrefix: 'upper',
            extraEncoding: tooltipEncoding,
        }),
        ...makeErrorBarPart({
            partName: 'rule',
            mark: {
                type: 'rule',
                ariaRoleDescription: 'errorbar',
                ...(thickness !== undefined ? { size: thickness } : {}),
            },
            positionPrefix: 'lower',
            endPositionPrefix: 'upper',
            extraEncoding: tooltipEncoding,
        }),
    ];
    return {
        ...outerSpec,
        transform,
        ...(layer.length > 1 ? { layer } : { ...layer[0] }),
    };
}
function errorBarOrientAndInputType(spec, compositeMark) {
    const { encoding } = spec;
    if (errorBarIsInputTypeRaw(encoding)) {
        return {
            orient: compositeMarkOrient(spec, compositeMark),
            inputType: 'raw',
        };
    }
    const isTypeAggregatedUpperLower = errorBarIsInputTypeAggregatedUpperLower(encoding);
    const isTypeAggregatedError = errorBarIsInputTypeAggregatedError(encoding);
    const x = encoding.x;
    const y = encoding.y;
    if (isTypeAggregatedUpperLower) {
        // type is aggregated-upper-lower
        if (isTypeAggregatedError) {
            throw new Error(`${compositeMark} cannot be both type aggregated-upper-lower and aggregated-error`);
        }
        const x2 = encoding.x2;
        const y2 = encoding.y2;
        if (isFieldOrDatumDef(x2) && isFieldOrDatumDef(y2)) {
            // having both x, x2 and y, y2
            throw new Error(`${compositeMark} cannot have both x2 and y2`);
        }
        else if (isFieldOrDatumDef(x2)) {
            if (isContinuousFieldOrDatumDef(x)) {
                // having x, x2 quantitative and field y, y2 are not specified
                return { orient: 'horizontal', inputType: 'aggregated-upper-lower' };
            }
            else {
                // having x, x2 that are not both quantitative
                throw new Error(`Both x and x2 have to be quantitative in ${compositeMark}`);
            }
        }
        else if (isFieldOrDatumDef(y2)) {
            // y2 is a FieldDef
            if (isContinuousFieldOrDatumDef(y)) {
                // having y, y2 quantitative and field x, x2 are not specified
                return { orient: 'vertical', inputType: 'aggregated-upper-lower' };
            }
            else {
                // having y, y2 that are not both quantitative
                throw new Error(`Both y and y2 have to be quantitative in ${compositeMark}`);
            }
        }
        throw new Error('No ranged axis');
    }
    else {
        // type is aggregated-error
        const xError = encoding.xError;
        const xError2 = encoding.xError2;
        const yError = encoding.yError;
        const yError2 = encoding.yError2;
        if (isFieldOrDatumDef(xError2) && !isFieldOrDatumDef(xError)) {
            // having xError2 without xError
            throw new Error(`${compositeMark} cannot have xError2 without xError`);
        }
        if (isFieldOrDatumDef(yError2) && !isFieldOrDatumDef(yError)) {
            // having yError2 without yError
            throw new Error(`${compositeMark} cannot have yError2 without yError`);
        }
        if (isFieldOrDatumDef(xError) && isFieldOrDatumDef(yError)) {
            // having both xError and yError
            throw new Error(`${compositeMark} cannot have both xError and yError with both are quantiative`);
        }
        else if (isFieldOrDatumDef(xError)) {
            if (isContinuousFieldOrDatumDef(x)) {
                // having x and xError that are all quantitative
                return { orient: 'horizontal', inputType: 'aggregated-error' };
            }
            else {
                // having x, xError, and xError2 that are not all quantitative
                throw new Error('All x, xError, and xError2 (if exist) have to be quantitative');
            }
        }
        else if (isFieldOrDatumDef(yError)) {
            if (isContinuousFieldOrDatumDef(y)) {
                // having y and yError that are all quantitative
                return { orient: 'vertical', inputType: 'aggregated-error' };
            }
            else {
                // having y, yError, and yError2 that are not all quantitative
                throw new Error('All y, yError, and yError2 (if exist) have to be quantitative');
            }
        }
        throw new Error('No ranged axis');
    }
}
function errorBarIsInputTypeRaw(encoding) {
    return ((isFieldOrDatumDef(encoding.x) || isFieldOrDatumDef(encoding.y)) &&
        !isFieldOrDatumDef(encoding.x2) &&
        !isFieldOrDatumDef(encoding.y2) &&
        !isFieldOrDatumDef(encoding.xError) &&
        !isFieldOrDatumDef(encoding.xError2) &&
        !isFieldOrDatumDef(encoding.yError) &&
        !isFieldOrDatumDef(encoding.yError2));
}
function errorBarIsInputTypeAggregatedUpperLower(encoding) {
    return isFieldOrDatumDef(encoding.x2) || isFieldOrDatumDef(encoding.y2);
}
function errorBarIsInputTypeAggregatedError(encoding) {
    return (isFieldOrDatumDef(encoding.xError) ||
        isFieldOrDatumDef(encoding.xError2) ||
        isFieldOrDatumDef(encoding.yError) ||
        isFieldOrDatumDef(encoding.yError2));
}
function errorBarParams(spec, compositeMark, config) {
    // TODO: use selection
    const { mark, encoding, params, projection: _p, ...outerSpec } = spec;
    const markDef = isMarkDef(mark) ? mark : { type: mark };
    // TODO(https://github.com/vega/vega-lite/issues/3702): add selection support
    if (params) {
        warn(selectionNotSupported(compositeMark));
    }
    const { orient, inputType } = errorBarOrientAndInputType(spec, compositeMark);
    const { continuousAxisChannelDef, continuousAxisChannelDef2, continuousAxisChannelDefError, continuousAxisChannelDefError2, continuousAxis, } = compositeMarkContinuousAxis(spec, orient, compositeMark);
    const { errorBarSpecificAggregate, postAggregateCalculates, tooltipSummary, tooltipTitleWithFieldName } = errorBarAggregationAndCalculation(markDef, continuousAxisChannelDef, continuousAxisChannelDef2, continuousAxisChannelDefError, continuousAxisChannelDefError2, inputType, compositeMark, config);
    const { [continuousAxis]: oldContinuousAxisChannelDef, [continuousAxis === 'x' ? 'x2' : 'y2']: oldContinuousAxisChannelDef2, [continuousAxis === 'x' ? 'xError' : 'yError']: oldContinuousAxisChannelDefError, [continuousAxis === 'x' ? 'xError2' : 'yError2']: oldContinuousAxisChannelDefError2, ...oldEncodingWithoutContinuousAxis } = encoding;
    const { bins, timeUnits, aggregate: oldAggregate, groupby: oldGroupBy, encoding: encodingWithoutContinuousAxis, } = extractTransformsFromEncoding(oldEncodingWithoutContinuousAxis, config);
    const aggregate = [...oldAggregate, ...errorBarSpecificAggregate];
    const groupby = inputType !== 'raw' ? [] : oldGroupBy;
    const tooltipEncoding = getCompositeMarkTooltip(tooltipSummary, continuousAxisChannelDef, encodingWithoutContinuousAxis, tooltipTitleWithFieldName);
    return {
        transform: [
            ...(outerSpec.transform ?? []),
            ...bins,
            ...timeUnits,
            ...(aggregate.length === 0 ? [] : [{ aggregate, groupby }]),
            ...postAggregateCalculates,
        ],
        groupby,
        continuousAxisChannelDef,
        continuousAxis,
        encodingWithoutContinuousAxis,
        ticksOrient: orient === 'vertical' ? 'horizontal' : 'vertical',
        markDef,
        outerSpec,
        tooltipEncoding,
    };
}
function errorBarAggregationAndCalculation(markDef, continuousAxisChannelDef, continuousAxisChannelDef2, continuousAxisChannelDefError, continuousAxisChannelDefError2, inputType, compositeMark, config) {
    let errorBarSpecificAggregate = [];
    let postAggregateCalculates = [];
    const continuousFieldName = continuousAxisChannelDef.field;
    let tooltipSummary;
    let tooltipTitleWithFieldName = false;
    if (inputType === 'raw') {
        const center = markDef.center
            ? markDef.center
            : markDef.extent
                ? markDef.extent === 'iqr'
                    ? 'median'
                    : 'mean'
                : config.errorbar.center;
        const extent = markDef.extent ? markDef.extent : center === 'mean' ? 'stderr' : 'iqr';
        if ((center === 'median') !== (extent === 'iqr')) {
            warn(errorBarCenterIsUsedWithWrongExtent(center, extent, compositeMark));
        }
        if (extent === 'stderr' || extent === 'stdev') {
            errorBarSpecificAggregate = [
                { op: extent, field: continuousFieldName, as: `extent_${continuousFieldName}` },
                { op: center, field: continuousFieldName, as: `center_${continuousFieldName}` },
            ];
            postAggregateCalculates = [
                {
                    calculate: `${accessWithDatumToUnescapedPath(`center_${continuousFieldName}`)} + ${accessWithDatumToUnescapedPath(`extent_${continuousFieldName}`)}`,
                    as: `upper_${continuousFieldName}`,
                },
                {
                    calculate: `${accessWithDatumToUnescapedPath(`center_${continuousFieldName}`)} - ${accessWithDatumToUnescapedPath(`extent_${continuousFieldName}`)}`,
                    as: `lower_${continuousFieldName}`,
                },
            ];
            tooltipSummary = [
                { fieldPrefix: 'center_', titlePrefix: titleCase(center) },
                { fieldPrefix: 'upper_', titlePrefix: getTitlePrefix(center, extent, '+') },
                { fieldPrefix: 'lower_', titlePrefix: getTitlePrefix(center, extent, '-') },
            ];
            tooltipTitleWithFieldName = true;
        }
        else {
            let centerOp;
            let lowerExtentOp;
            let upperExtentOp;
            if (extent === 'ci') {
                centerOp = 'mean';
                lowerExtentOp = 'ci0';
                upperExtentOp = 'ci1';
            }
            else {
                centerOp = 'median';
                lowerExtentOp = 'q1';
                upperExtentOp = 'q3';
            }
            errorBarSpecificAggregate = [
                { op: lowerExtentOp, field: continuousFieldName, as: `lower_${continuousFieldName}` },
                { op: upperExtentOp, field: continuousFieldName, as: `upper_${continuousFieldName}` },
                { op: centerOp, field: continuousFieldName, as: `center_${continuousFieldName}` },
            ];
            tooltipSummary = [
                {
                    fieldPrefix: 'upper_',
                    titlePrefix: title({ field: continuousFieldName, aggregate: upperExtentOp, type: 'quantitative' }, config, {
                        allowDisabling: false,
                    }),
                },
                {
                    fieldPrefix: 'lower_',
                    titlePrefix: title({ field: continuousFieldName, aggregate: lowerExtentOp, type: 'quantitative' }, config, {
                        allowDisabling: false,
                    }),
                },
                {
                    fieldPrefix: 'center_',
                    titlePrefix: title({ field: continuousFieldName, aggregate: centerOp, type: 'quantitative' }, config, {
                        allowDisabling: false,
                    }),
                },
            ];
        }
    }
    else {
        if (markDef.center || markDef.extent) {
            warn(errorBarCenterAndExtentAreNotNeeded(markDef.center, markDef.extent));
        }
        if (inputType === 'aggregated-upper-lower') {
            tooltipSummary = [];
            postAggregateCalculates = [
                {
                    calculate: accessWithDatumToUnescapedPath(continuousAxisChannelDef2.field),
                    as: `upper_${continuousFieldName}`,
                },
                { calculate: accessWithDatumToUnescapedPath(continuousFieldName), as: `lower_${continuousFieldName}` },
            ];
        }
        else if (inputType === 'aggregated-error') {
            tooltipSummary = [{ fieldPrefix: '', titlePrefix: continuousFieldName }];
            postAggregateCalculates = [
                {
                    calculate: `${accessWithDatumToUnescapedPath(continuousFieldName)} + ${accessWithDatumToUnescapedPath(continuousAxisChannelDefError.field)}`,
                    as: `upper_${continuousFieldName}`,
                },
            ];
            if (continuousAxisChannelDefError2) {
                postAggregateCalculates.push({
                    calculate: `${accessWithDatumToUnescapedPath(continuousFieldName)} + ${accessWithDatumToUnescapedPath(continuousAxisChannelDefError2.field)}`,
                    as: `lower_${continuousFieldName}`,
                });
            }
            else {
                postAggregateCalculates.push({
                    calculate: `${accessWithDatumToUnescapedPath(continuousFieldName)} - ${accessWithDatumToUnescapedPath(continuousAxisChannelDefError.field)}`,
                    as: `lower_${continuousFieldName}`,
                });
            }
        }
        for (const postAggregateCalculate of postAggregateCalculates) {
            tooltipSummary.push({
                fieldPrefix: postAggregateCalculate.as.substring(0, 6),
                titlePrefix: replaceAll(replaceAll(postAggregateCalculate.calculate, "datum['", ''), "']", ''),
            });
        }
    }
    return { postAggregateCalculates, errorBarSpecificAggregate, tooltipSummary, tooltipTitleWithFieldName };
}
function getTitlePrefix(center, extent, operation) {
    return `${titleCase(center)} ${operation} ${extent}`;
}

const ERRORBAND = 'errorband';
const ERRORBAND_PARTS = ['band', 'borders'];
const errorBandNormalizer = new CompositeMarkNormalizer(ERRORBAND, normalizeErrorBand);
function normalizeErrorBand(spec, { config }) {
    // Need to initEncoding first so we can infer type
    spec = {
        ...spec,
        encoding: normalizeEncoding(spec.encoding, config),
    };
    const { transform, continuousAxisChannelDef, continuousAxis, encodingWithoutContinuousAxis, markDef, outerSpec, tooltipEncoding, } = errorBarParams(spec, ERRORBAND, config);
    const errorBandDef = markDef;
    const makeErrorBandPart = makeCompositeAggregatePartFactory(errorBandDef, continuousAxis, continuousAxisChannelDef, encodingWithoutContinuousAxis, config.errorband);
    const is2D = spec.encoding.x !== undefined && spec.encoding.y !== undefined;
    let bandMark = { type: is2D ? 'area' : 'rect' };
    let bordersMark = { type: is2D ? 'line' : 'rule' };
    const interpolate = {
        ...(errorBandDef.interpolate ? { interpolate: errorBandDef.interpolate } : {}),
        ...(errorBandDef.tension && errorBandDef.interpolate ? { tension: errorBandDef.tension } : {}),
    };
    if (is2D) {
        bandMark = {
            ...bandMark,
            ...interpolate,
            ariaRoleDescription: 'errorband',
        };
        bordersMark = {
            ...bordersMark,
            ...interpolate,
            aria: false,
        };
    }
    else if (errorBandDef.interpolate) {
        warn(errorBand1DNotSupport('interpolate'));
    }
    else if (errorBandDef.tension) {
        warn(errorBand1DNotSupport('tension'));
    }
    return {
        ...outerSpec,
        transform,
        layer: [
            ...makeErrorBandPart({
                partName: 'band',
                mark: bandMark,
                positionPrefix: 'lower',
                endPositionPrefix: 'upper',
                extraEncoding: tooltipEncoding,
            }),
            ...makeErrorBandPart({
                partName: 'borders',
                mark: bordersMark,
                positionPrefix: 'lower',
                extraEncoding: tooltipEncoding,
            }),
            ...makeErrorBandPart({
                partName: 'borders',
                mark: bordersMark,
                positionPrefix: 'upper',
                extraEncoding: tooltipEncoding,
            }),
        ],
    };
}

/**
 * Registry index for all composite mark's normalizer
 */
const compositeMarkRegistry = {};
function add(mark, run, parts) {
    const normalizer = new CompositeMarkNormalizer(mark, run);
    compositeMarkRegistry[mark] = { normalizer, parts };
}
function getAllCompositeMarks() {
    return keys(compositeMarkRegistry);
}
add(BOXPLOT, normalizeBoxPlot, BOXPLOT_PARTS);
add(ERRORBAR, normalizeErrorBar, ERRORBAR_PARTS);
add(ERRORBAND, normalizeErrorBand, ERRORBAND_PARTS);

const VL_ONLY_LEGEND_CONFIG = [
    'gradientHorizontalMaxLength',
    'gradientHorizontalMinLength',
    'gradientVerticalMaxLength',
    'gradientVerticalMinLength',
    'unselectedOpacity',
];

const HEADER_TITLE_PROPERTIES_MAP = {
    titleAlign: 'align',
    titleAnchor: 'anchor',
    titleAngle: 'angle',
    titleBaseline: 'baseline',
    titleColor: 'color',
    titleFont: 'font',
    titleFontSize: 'fontSize',
    titleFontStyle: 'fontStyle',
    titleFontWeight: 'fontWeight',
    titleLimit: 'limit',
    titleLineHeight: 'lineHeight',
    titleOrient: 'orient',
    titlePadding: 'offset',
};
const HEADER_LABEL_PROPERTIES_MAP = {
    labelAlign: 'align',
    labelAnchor: 'anchor',
    labelAngle: 'angle',
    labelBaseline: 'baseline',
    labelColor: 'color',
    labelFont: 'font',
    labelFontSize: 'fontSize',
    labelFontStyle: 'fontStyle',
    labelFontWeight: 'fontWeight',
    labelLimit: 'limit',
    labelLineHeight: 'lineHeight',
    labelOrient: 'orient',
    labelPadding: 'offset',
};
const HEADER_TITLE_PROPERTIES = keys(HEADER_TITLE_PROPERTIES_MAP);
const HEADER_LABEL_PROPERTIES = keys(HEADER_LABEL_PROPERTIES_MAP);
const HEADER_CONFIGS_INDEX = {
    header: 1,
    headerRow: 1,
    headerColumn: 1,
    headerFacet: 1,
};
const HEADER_CONFIGS = keys(HEADER_CONFIGS_INDEX);

const LEGEND_SCALE_CHANNELS = [
    'size',
    'shape',
    'fill',
    'stroke',
    'strokeDash',
    'strokeWidth',
    'opacity',
];
const defaultLegendConfig = {
    gradientHorizontalMaxLength: 200,
    gradientHorizontalMinLength: 100,
    gradientVerticalMaxLength: 200,
    gradientVerticalMinLength: 64, // This is Vega's minimum.
    unselectedOpacity: 0.35,
};
const COMMON_LEGEND_PROPERTY_INDEX = {
    aria: 1,
    clipHeight: 1,
    columnPadding: 1,
    columns: 1,
    cornerRadius: 1,
    description: 1,
    direction: 1,
    fillColor: 1,
    format: 1,
    formatType: 1,
    gradientLength: 1,
    gradientOpacity: 1,
    gradientStrokeColor: 1,
    gradientStrokeWidth: 1,
    gradientThickness: 1,
    gridAlign: 1,
    labelAlign: 1,
    labelBaseline: 1,
    labelColor: 1,
    labelFont: 1,
    labelFontSize: 1,
    labelFontStyle: 1,
    labelFontWeight: 1,
    labelLimit: 1,
    labelOffset: 1,
    labelOpacity: 1,
    labelOverlap: 1,
    labelPadding: 1,
    labelSeparation: 1,
    legendX: 1,
    legendY: 1,
    offset: 1,
    orient: 1,
    padding: 1,
    rowPadding: 1,
    strokeColor: 1,
    symbolDash: 1,
    symbolDashOffset: 1,
    symbolFillColor: 1,
    symbolLimit: 1,
    symbolOffset: 1,
    symbolOpacity: 1,
    symbolSize: 1,
    symbolStrokeColor: 1,
    symbolStrokeWidth: 1,
    symbolType: 1,
    tickCount: 1,
    tickMinStep: 1,
    title: 1,
    titleAlign: 1,
    titleAnchor: 1,
    titleBaseline: 1,
    titleColor: 1,
    titleFont: 1,
    titleFontSize: 1,
    titleFontStyle: 1,
    titleFontWeight: 1,
    titleLimit: 1,
    titleLineHeight: 1,
    titleOpacity: 1,
    titleOrient: 1,
    titlePadding: 1,
    type: 1,
    values: 1,
    zindex: 1,
};

const SELECTION_ID = '_vgsid_';
const defaultConfig$1 = {
    point: {
        on: 'click',
        fields: [SELECTION_ID],
        toggle: 'event.shiftKey',
        resolve: 'global',
        clear: 'dblclick',
    },
    interval: {
        on: '[pointerdown, window:pointerup] > window:pointermove!',
        encodings: ['x', 'y'],
        translate: '[pointerdown, window:pointerup] > window:pointermove!',
        zoom: 'wheel!',
        mark: { fill: '#333', fillOpacity: 0.125, stroke: 'white' },
        resolve: 'global',
        clear: 'dblclick',
    },
};
function isLegendBinding(bind) {
    return bind === 'legend' || !!bind?.legend;
}
function isLegendStreamBinding(bind) {
    return isLegendBinding(bind) && isObject(bind);
}
function isSelectionParameter(param) {
    return !!param?.['select'];
}

function assembleParameterSignals(params) {
    const signals = [];
    for (const param of params || []) {
        // Selection parameters are handled separately via assembleSelectionTopLevelSignals
        // and assembleSignals methods registered on the Model.
        if (isSelectionParameter(param))
            continue;
        const { expr, bind, ...rest } = param;
        if (bind && expr) {
            // Vega's InitSignal -- apply expr to "init"
            const signal = {
                ...rest,
                bind,
                init: expr,
            };
            signals.push(signal);
        }
        else {
            const signal = {
                ...rest,
                ...(expr ? { update: expr } : {}),
                ...(bind ? { bind } : {}),
            };
            signals.push(signal);
        }
    }
    return signals;
}

function isAnyConcatSpec(spec) {
    return isVConcatSpec(spec) || isHConcatSpec(spec) || isConcatSpec(spec);
}
function isConcatSpec(spec) {
    return hasProperty(spec, 'concat');
}
function isVConcatSpec(spec) {
    return hasProperty(spec, 'vconcat');
}
function isHConcatSpec(spec) {
    return hasProperty(spec, 'hconcat');
}

function getStepFor({ step, offsetIsDiscrete }) {
    if (offsetIsDiscrete) {
        return step.for ?? 'offset';
    }
    else {
        return 'position';
    }
}
function isStep(size) {
    return hasProperty(size, 'step');
}
function isFrameMixins(o) {
    return hasProperty(o, 'view') || hasProperty(o, 'width') || hasProperty(o, 'height');
}
const DEFAULT_SPACING = 20;
const COMPOSITION_LAYOUT_INDEX = {
    align: 1,
    bounds: 1,
    center: 1,
    columns: 1,
    spacing: 1,
};
const COMPOSITION_LAYOUT_PROPERTIES = keys(COMPOSITION_LAYOUT_INDEX);
function extractCompositionLayout(spec, specType, config) {
    const compositionConfig = config[specType];
    const layout = {};
    // Apply config first
    const { spacing: spacingConfig, columns } = compositionConfig;
    if (spacingConfig !== undefined) {
        layout.spacing = spacingConfig;
    }
    if (columns !== undefined) {
        if ((isFacetSpec(spec) && !isFacetMapping(spec.facet)) || isConcatSpec(spec)) {
            layout.columns = columns;
        }
    }
    if (isVConcatSpec(spec)) {
        layout.columns = 1;
    }
    // Then copy properties from the spec
    for (const prop of COMPOSITION_LAYOUT_PROPERTIES) {
        if (spec[prop] !== undefined) {
            if (prop === 'spacing') {
                const spacing = spec[prop];
                layout[prop] = isNumber$1(spacing)
                    ? spacing
                    : {
                        row: spacing.row ?? spacingConfig,
                        column: spacing.column ?? spacingConfig,
                    };
            }
            else {
                layout[prop] = spec[prop];
            }
        }
    }
    return layout;
}

function getViewConfigContinuousSize(viewConfig, channel) {
    return viewConfig[channel] ?? viewConfig[channel === 'width' ? 'continuousWidth' : 'continuousHeight']; // get width/height for backwards compatibility
}
function getViewConfigDiscreteStep(viewConfig, channel) {
    const size = getViewConfigDiscreteSize(viewConfig, channel);
    return isStep(size) ? size.step : DEFAULT_STEP;
}
function getViewConfigDiscreteSize(viewConfig, channel) {
    const size = viewConfig[channel] ?? viewConfig[channel === 'width' ? 'discreteWidth' : 'discreteHeight']; // get width/height for backwards compatibility
    return getFirstDefined(size, { step: viewConfig.step });
}
const DEFAULT_STEP = 20;
const defaultViewConfig = {
    continuousWidth: 300,
    continuousHeight: 300,
    step: DEFAULT_STEP,
};
const defaultConfig = {
    background: 'white',
    padding: 5,
    timeFormat: '%b %d, %Y',
    countTitle: 'Count of Records',
    view: defaultViewConfig,
    mark: defaultMarkConfig,
    arc: {},
    area: {},
    bar: defaultBarConfig,
    circle: {},
    geoshape: {},
    image: {},
    line: {},
    point: {},
    rect: defaultRectConfig,
    rule: { color: 'black' }, // Need this to override default color in mark config
    square: {},
    text: { color: 'black' }, // Need this to override default color in mark config
    tick: defaultTickConfig,
    trail: {},
    boxplot: {
        size: 14,
        extent: 1.5,
        box: {},
        median: { color: 'white' },
        outliers: {},
        rule: {},
        ticks: null,
    },
    errorbar: {
        center: 'mean',
        rule: true,
        ticks: false,
    },
    errorband: {
        band: {
            opacity: 0.3,
        },
        borders: false,
    },
    scale: defaultScaleConfig,
    projection: {},
    legend: defaultLegendConfig,
    header: { titlePadding: 10, labelPadding: 10 },
    headerColumn: {},
    headerRow: {},
    headerFacet: {},
    selection: defaultConfig$1,
    style: {},
    title: {},
    facet: { spacing: DEFAULT_SPACING },
    concat: { spacing: DEFAULT_SPACING },
    normalizedNumberFormat: '.0%',
};
// Tableau10 color palette, copied from `vegaScale.scheme('tableau10')`
const tab10 = [
    '#4c78a8',
    '#f58518',
    '#e45756',
    '#72b7b2',
    '#54a24b',
    '#eeca3b',
    '#b279a2',
    '#ff9da6',
    '#9d755d',
    '#bab0ac',
];
const DEFAULT_FONT_SIZE = {
    text: 11,
    guideLabel: 10,
    guideTitle: 11,
    groupTitle: 13,
    groupSubtitle: 12,
};
const DEFAULT_COLOR = {
    blue: tab10[0],
    orange: tab10[1],
    red: tab10[2],
    teal: tab10[3],
    green: tab10[4],
    yellow: tab10[5],
    purple: tab10[6],
    pink: tab10[7],
    brown: tab10[8],
    gray0: '#000',
    gray1: '#111',
    gray2: '#222',
    gray3: '#333',
    gray4: '#444',
    gray5: '#555',
    gray6: '#666',
    gray7: '#777',
    gray8: '#888',
    gray9: '#999',
    gray10: '#aaa',
    gray11: '#bbb',
    gray12: '#ccc',
    gray13: '#ddd',
    gray14: '#eee',
    gray15: '#fff',
};
function colorSignalConfig(color = {}) {
    return {
        signals: [
            {
                name: 'color',
                value: isObject(color) ? { ...DEFAULT_COLOR, ...color } : DEFAULT_COLOR,
            },
        ],
        mark: { color: { signal: 'color.blue' } },
        rule: { color: { signal: 'color.gray0' } },
        text: {
            color: { signal: 'color.gray0' },
        },
        style: {
            'guide-label': {
                fill: { signal: 'color.gray0' },
            },
            'guide-title': {
                fill: { signal: 'color.gray0' },
            },
            'group-title': {
                fill: { signal: 'color.gray0' },
            },
            'group-subtitle': {
                fill: { signal: 'color.gray0' },
            },
            cell: {
                stroke: { signal: 'color.gray8' },
            },
        },
        axis: {
            domainColor: { signal: 'color.gray13' },
            gridColor: { signal: 'color.gray8' },
            tickColor: { signal: 'color.gray13' },
        },
        range: {
            category: [
                { signal: 'color.blue' },
                { signal: 'color.orange' },
                { signal: 'color.red' },
                { signal: 'color.teal' },
                { signal: 'color.green' },
                { signal: 'color.yellow' },
                { signal: 'color.purple' },
                { signal: 'color.pink' },
                { signal: 'color.brown' },
                { signal: 'color.grey8' },
            ],
        },
    };
}
function fontSizeSignalConfig(fontSize) {
    return {
        signals: [
            {
                name: 'fontSize',
                value: isObject(fontSize) ? { ...DEFAULT_FONT_SIZE, ...fontSize } : DEFAULT_FONT_SIZE,
            },
        ],
        text: {
            fontSize: { signal: 'fontSize.text' },
        },
        style: {
            'guide-label': {
                fontSize: { signal: 'fontSize.guideLabel' },
            },
            'guide-title': {
                fontSize: { signal: 'fontSize.guideTitle' },
            },
            'group-title': {
                fontSize: { signal: 'fontSize.groupTitle' },
            },
            'group-subtitle': {
                fontSize: { signal: 'fontSize.groupSubtitle' },
            },
        },
    };
}
function fontConfig(font) {
    return {
        text: { font },
        style: {
            'guide-label': { font },
            'guide-title': { font },
            'group-title': { font },
            'group-subtitle': { font },
        },
    };
}
function getAxisConfigInternal(axisConfig) {
    const props = keys(axisConfig || {});
    const axisConfigInternal = {};
    for (const prop of props) {
        const val = axisConfig[prop];
        axisConfigInternal[prop] = isConditionalAxisValue(val)
            ? signalOrValueRefWithCondition(val)
            : signalRefOrValue(val);
    }
    return axisConfigInternal;
}
function getStyleConfigInternal(styleConfig) {
    const props = keys(styleConfig);
    const styleConfigInternal = {};
    for (const prop of props) {
        // We need to cast to cheat a bit here since styleConfig can be either mark config or axis config
        styleConfigInternal[prop] = getAxisConfigInternal(styleConfig[prop]);
    }
    return styleConfigInternal;
}
const configPropsWithExpr = [
    ...MARK_CONFIGS,
    ...AXIS_CONFIGS,
    ...HEADER_CONFIGS,
    'background',
    'padding',
    'legend',
    'lineBreak',
    'scale',
    'style',
    'title',
    'view',
];
/**
 * Merge specified config with default config and config for the `color` flag,
 * then replace all expressions with signals
 */
function initConfig(specifiedConfig = {}) {
    const { color, font, fontSize, selection, ...restConfig } = specifiedConfig;
    const mergedConfig = mergeConfig({}, duplicate(defaultConfig), font ? fontConfig(font) : {}, color ? colorSignalConfig(color) : {}, fontSize ? fontSizeSignalConfig(fontSize) : {}, restConfig || {});
    // mergeConfig doesn't recurse and overrides object values.
    if (selection) {
        writeConfig(mergedConfig, 'selection', selection, true);
    }
    const outputConfig = omit(mergedConfig, configPropsWithExpr);
    for (const prop of ['background', 'lineBreak', 'padding']) {
        if (mergedConfig[prop]) {
            outputConfig[prop] = signalRefOrValue(mergedConfig[prop]);
        }
    }
    for (const markConfigType of MARK_CONFIGS) {
        if (mergedConfig[markConfigType]) {
            // FIXME: outputConfig[markConfigType] expects that types are replaced recursively but replaceExprRef only replaces one level deep
            outputConfig[markConfigType] = replaceExprRef(mergedConfig[markConfigType]);
        }
    }
    for (const axisConfigType of AXIS_CONFIGS) {
        if (mergedConfig[axisConfigType]) {
            outputConfig[axisConfigType] = getAxisConfigInternal(mergedConfig[axisConfigType]);
        }
    }
    for (const headerConfigType of HEADER_CONFIGS) {
        if (mergedConfig[headerConfigType]) {
            outputConfig[headerConfigType] = replaceExprRef(mergedConfig[headerConfigType]);
        }
    }
    if (mergedConfig.legend) {
        outputConfig.legend = replaceExprRef(mergedConfig.legend);
    }
    if (mergedConfig.scale) {
        const { invalid, ...otherScaleConfig } = mergedConfig.scale;
        const newScaleInvalid = replaceExprRef(invalid, { level: 1 });
        outputConfig.scale = {
            ...replaceExprRef(otherScaleConfig),
            ...(keys(newScaleInvalid).length > 0 ? { invalid: newScaleInvalid } : {}),
        };
    }
    if (mergedConfig.style) {
        outputConfig.style = getStyleConfigInternal(mergedConfig.style);
    }
    if (mergedConfig.title) {
        outputConfig.title = replaceExprRef(mergedConfig.title);
    }
    if (mergedConfig.view) {
        outputConfig.view = replaceExprRef(mergedConfig.view);
    }
    return outputConfig;
}
const MARK_STYLES = new Set(['view', ...PRIMITIVE_MARKS]);
const VL_ONLY_CONFIG_PROPERTIES = [
    'color',
    'fontSize',
    'background', // We apply background to the spec directly.
    'padding',
    'facet',
    'concat',
    'numberFormat',
    'numberFormatType',
    'normalizedNumberFormat',
    'normalizedNumberFormatType',
    'timeFormat',
    'countTitle',
    'header',
    'axisQuantitative',
    'axisTemporal',
    'axisDiscrete',
    'axisPoint',
    'axisXBand',
    'axisXPoint',
    'axisXDiscrete',
    'axisXQuantitative',
    'axisXTemporal',
    'axisYBand',
    'axisYPoint',
    'axisYDiscrete',
    'axisYQuantitative',
    'axisYTemporal',
    'scale',
    'selection',
    'overlay',
];
const VL_ONLY_ALL_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX = {
    view: ['continuousWidth', 'continuousHeight', 'discreteWidth', 'discreteHeight', 'step'],
    ...VL_ONLY_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX,
};
function stripAndRedirectConfig(config) {
    config = duplicate(config);
    for (const prop of VL_ONLY_CONFIG_PROPERTIES) {
        delete config[prop];
    }
    if (config.axis) {
        // delete condition axis config
        for (const prop in config.axis) {
            if (isConditionalAxisValue(config.axis[prop])) {
                delete config.axis[prop];
            }
        }
    }
    if (config.legend) {
        for (const prop of VL_ONLY_LEGEND_CONFIG) {
            delete config.legend[prop];
        }
    }
    // Remove Vega-Lite only generic mark config
    if (config.mark) {
        for (const prop of VL_ONLY_MARK_CONFIG_PROPERTIES) {
            delete config.mark[prop];
        }
        if (config.mark.tooltip && isObject(config.mark.tooltip)) {
            delete config.mark.tooltip;
        }
    }
    if (config.params) {
        config.signals = (config.signals || []).concat(assembleParameterSignals(config.params));
        delete config.params;
    }
    for (const markType of MARK_STYLES) {
        // Remove Vega-Lite-only mark config
        for (const prop of VL_ONLY_MARK_CONFIG_PROPERTIES) {
            delete config[markType][prop];
        }
        // Remove Vega-Lite only mark-specific config
        const vlOnlyMarkSpecificConfigs = VL_ONLY_ALL_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX[markType];
        if (vlOnlyMarkSpecificConfigs) {
            for (const prop of vlOnlyMarkSpecificConfigs) {
                delete config[markType][prop];
            }
        }
        // Redirect mark config to config.style so that mark config only affect its own mark type
        // without affecting other marks that share the same underlying Vega marks.
        // For example, config.rect should not affect bar marks.
        redirectConfigToStyleConfig(config, markType);
    }
    for (const m of getAllCompositeMarks()) {
        // Clean up the composite mark config as we don't need them in the output specs anymore
        delete config[m];
    }
    redirectTitleConfig(config);
    // Remove empty config objects.
    for (const prop in config) {
        // @ts-expect-error(TS7053)
        if (isObject(config[prop]) && isEmpty(config[prop])) {
            // @ts-expect-error(TS7053)
            delete config[prop];
        }
    }
    return isEmpty(config) ? undefined : config;
}
/**
 *
 * Redirect config.title -- so that title config do not affect header labels,
 * which also uses `title` directive to implement.
 *
 * For subtitle configs in config.title, keep them in config.title as header titles never have subtitles.
 */
function redirectTitleConfig(config) {
    const { titleMarkConfig, subtitleMarkConfig, subtitle } = extractTitleConfig(config.title);
    // set config.style if title/subtitleMarkConfig is not an empty object
    if (!isEmpty(titleMarkConfig)) {
        config.style['group-title'] = {
            ...config.style['group-title'],
            ...titleMarkConfig, // config.title has higher precedence than config.style.group-title in Vega
        };
    }
    if (!isEmpty(subtitleMarkConfig)) {
        config.style['group-subtitle'] = {
            ...config.style['group-subtitle'],
            ...subtitleMarkConfig,
        };
    }
    // subtitle part can stay in config.title since header titles do not use subtitle
    if (!isEmpty(subtitle)) {
        config.title = subtitle;
    }
    else {
        delete config.title;
    }
}
function redirectConfigToStyleConfig(config, prop, // string = composite mark
toProp, compositeMarkPart) {
    const propConfig = config[prop];
    if (prop === 'view') {
        toProp = 'cell'; // View's default style is "cell"
    }
    const style = {
        ...propConfig,
        ...config.style[toProp ?? prop],
    };
    // set config.style if it is not an empty object
    if (!isEmpty(style)) {
        config.style[toProp ?? prop] = style;
    }
    {
        // For composite mark, so don't delete the whole config yet as we have to do multiple redirections.
        delete config[prop];
    }
}

function isLayerSpec(spec) {
    return hasProperty(spec, 'layer');
}

function isRepeatSpec(spec) {
    return hasProperty(spec, 'repeat');
}
function isLayerRepeatSpec(spec) {
    return !isArray(spec.repeat) && hasProperty(spec.repeat, 'layer');
}

class SpecMapper {
    map(spec, params) {
        if (isFacetSpec(spec)) {
            return this.mapFacet(spec, params);
        }
        else if (isRepeatSpec(spec)) {
            return this.mapRepeat(spec, params);
        }
        else if (isHConcatSpec(spec)) {
            return this.mapHConcat(spec, params);
        }
        else if (isVConcatSpec(spec)) {
            return this.mapVConcat(spec, params);
        }
        else if (isConcatSpec(spec)) {
            return this.mapConcat(spec, params);
        }
        else {
            return this.mapLayerOrUnit(spec, params);
        }
    }
    mapLayerOrUnit(spec, params) {
        if (isLayerSpec(spec)) {
            return this.mapLayer(spec, params);
        }
        else if (isUnitSpec(spec)) {
            return this.mapUnit(spec, params);
        }
        throw new Error(invalidSpec(spec));
    }
    mapLayer(spec, params) {
        return {
            ...spec,
            layer: spec.layer.map((subspec) => this.mapLayerOrUnit(subspec, params)),
        };
    }
    mapHConcat(spec, params) {
        return {
            ...spec,
            hconcat: spec.hconcat.map((subspec) => this.map(subspec, params)),
        };
    }
    mapVConcat(spec, params) {
        return {
            ...spec,
            vconcat: spec.vconcat.map((subspec) => this.map(subspec, params)),
        };
    }
    mapConcat(spec, params) {
        const { concat, ...rest } = spec;
        return {
            ...rest,
            concat: concat.map((subspec) => this.map(subspec, params)),
        };
    }
    mapFacet(spec, params) {
        return {
            // as any is required here since TS cannot infer that FO may only be FieldName or Field, but not RepeatRef
            ...spec,
            // TODO: remove "any" once we support all facet listed in https://github.com/vega/vega-lite/issues/2760
            spec: this.map(spec.spec, params),
        };
    }
    mapRepeat(spec, params) {
        return {
            ...spec,
            // as any is required here since TS cannot infer that the output type satisfies the input type
            spec: this.map(spec.spec, params),
        };
    }
}

const STACK_OFFSET_INDEX = {
    zero: 1,
    center: 1,
    normalize: 1,
};
function isStackOffset(s) {
    return has$1(STACK_OFFSET_INDEX, s);
}
const STACKABLE_MARKS = new Set([ARC, BAR, AREA, RULE, POINT, CIRCLE, SQUARE, LINE, TEXT, TICK]);
const STACK_BY_DEFAULT_MARKS = new Set([BAR, AREA, ARC]);
function isUnbinnedQuantitative(channelDef) {
    return isFieldDef(channelDef) && channelDefType(channelDef) === 'quantitative' && !channelDef.bin;
}
function potentialStackedChannel(encoding, x, { orient, type: mark }) {
    const y = x === 'x' ? 'y' : 'radius';
    const isCartesianBarOrArea = x === 'x' && ['bar', 'area'].includes(mark);
    const xDef = encoding[x];
    const yDef = encoding[y];
    if (isFieldDef(xDef) && isFieldDef(yDef)) {
        if (isUnbinnedQuantitative(xDef) && isUnbinnedQuantitative(yDef)) {
            if (xDef.stack) {
                return x;
            }
            else if (yDef.stack) {
                return y;
            }
            const xAggregate = isFieldDef(xDef) && !!xDef.aggregate;
            const yAggregate = isFieldDef(yDef) && !!yDef.aggregate;
            // if there is no explicit stacking, only apply stack if there is only one aggregate for x or y
            if (xAggregate !== yAggregate) {
                return xAggregate ? x : y;
            }
            if (isCartesianBarOrArea) {
                if (orient === 'vertical') {
                    return y;
                }
                else if (orient === 'horizontal') {
                    return x;
                }
            }
        }
        else if (isUnbinnedQuantitative(xDef)) {
            return x;
        }
        else if (isUnbinnedQuantitative(yDef)) {
            return y;
        }
    }
    else if (isUnbinnedQuantitative(xDef)) {
        if (isCartesianBarOrArea && orient === 'vertical') {
            return undefined;
        }
        return x;
    }
    else if (isUnbinnedQuantitative(yDef)) {
        if (isCartesianBarOrArea && orient === 'horizontal') {
            return undefined;
        }
        return y;
    }
    return undefined;
}
function getDimensionChannel(channel) {
    switch (channel) {
        case 'x':
            return 'y';
        case 'y':
            return 'x';
        case 'theta':
            return 'radius';
        case 'radius':
            return 'theta';
    }
}
function stack(m, encoding) {
    const markDef = isMarkDef(m) ? m : { type: m };
    const mark = markDef.type;
    // Should have stackable mark
    if (!STACKABLE_MARKS.has(mark)) {
        return null;
    }
    // Run potential stacked twice, one for Cartesian and another for Polar,
    // so text marks can be stacked in any of the coordinates.
    // Note: The logic here is not perfectly correct.  If we want to support stacked dot plots where each dot is a pie chart with label, we have to change the stack logic here to separate Cartesian stacking for polar stacking.
    // However, since we probably never want to do that, let's just note the limitation here.
    const fieldChannel = potentialStackedChannel(encoding, 'x', markDef) || potentialStackedChannel(encoding, 'theta', markDef);
    if (!fieldChannel) {
        return null;
    }
    const stackedFieldDef = encoding[fieldChannel];
    const stackedField = isFieldDef(stackedFieldDef) ? vgField(stackedFieldDef, {}) : undefined;
    const dimensionChannel = getDimensionChannel(fieldChannel);
    const groupbyChannels = [];
    const groupbyFields = new Set();
    if (encoding[dimensionChannel]) {
        const dimensionDef = encoding[dimensionChannel];
        const dimensionField = isFieldDef(dimensionDef) ? vgField(dimensionDef, {}) : undefined;
        if (dimensionField && dimensionField !== stackedField) {
            // avoid grouping by the stacked field
            groupbyChannels.push(dimensionChannel);
            groupbyFields.add(dimensionField);
        }
    }
    const dimensionOffsetChannel = dimensionChannel === 'x' ? 'xOffset' : 'yOffset';
    const dimensionOffsetDef = encoding[dimensionOffsetChannel];
    const dimensionOffsetField = isFieldDef(dimensionOffsetDef) ? vgField(dimensionOffsetDef, {}) : undefined;
    if (dimensionOffsetField && dimensionOffsetField !== stackedField) {
        // avoid grouping by the stacked field
        groupbyChannels.push(dimensionOffsetChannel);
        groupbyFields.add(dimensionOffsetField);
    }
    // If the dimension has offset, don't stack anymore
    // Should have grouping level of detail that is different from the dimension field
    const stackBy = NONPOSITION_CHANNELS.reduce((sc, channel) => {
        // Ignore tooltip in stackBy (https://github.com/vega/vega-lite/issues/4001)
        if (channel !== 'tooltip' && channelHasField(encoding, channel)) {
            const channelDef = encoding[channel];
            for (const cDef of array$2(channelDef)) {
                const fieldDef = getFieldDef(cDef);
                if (fieldDef.aggregate) {
                    continue;
                }
                // Check whether the channel's field is identical to x/y's field or if the channel is a repeat
                const f = vgField(fieldDef, {});
                if (
                // if fielddef is a repeat, just include it in the stack by
                !f ||
                    // otherwise, the field must be different from the groupBy fields.
                    !groupbyFields.has(f)) {
                    sc.push({ channel, fieldDef });
                }
            }
        }
        return sc;
    }, []);
    // Automatically determine offset
    let offset;
    if (stackedFieldDef.stack !== undefined) {
        if (isBoolean$2(stackedFieldDef.stack)) {
            offset = stackedFieldDef.stack ? 'zero' : null;
        }
        else {
            offset = stackedFieldDef.stack;
        }
    }
    else if (STACK_BY_DEFAULT_MARKS.has(mark)) {
        offset = 'zero';
    }
    if (!offset || !isStackOffset(offset)) {
        return null;
    }
    if (isAggregate$1(encoding) && stackBy.length === 0) {
        return null;
    }
    // warn when stacking non-linear
    if (stackedFieldDef?.scale?.type && stackedFieldDef?.scale?.type !== ScaleType.LINEAR) {
        if (stackedFieldDef?.stack) {
            warn(stackNonLinearScale(stackedFieldDef.scale.type));
        }
    }
    // Check if it is a ranged mark
    if (isFieldOrDatumDef(encoding[getSecondaryRangeChannel(fieldChannel)])) {
        if (stackedFieldDef.stack !== undefined) {
            warn(cannotStackRangedMark(fieldChannel));
        }
        return null;
    }
    // Warn if stacking non-summative aggregate
    if (isFieldDef(stackedFieldDef) &&
        stackedFieldDef.aggregate &&
        !SUM_OPS.has(stackedFieldDef.aggregate)) {
        warn(stackNonSummativeAggregate(stackedFieldDef.aggregate));
    }
    return {
        groupbyChannels,
        groupbyFields,
        fieldChannel,
        impute: stackedFieldDef.impute === null ? false : isPathMark(mark),
        stackBy,
        offset,
    };
}

function initMarkdef(originalMarkDef, encoding, config) {
    // FIXME: markDef expects that exprRefs are replaced recursively but replaceExprRef only replaces the top level
    const markDef = replaceExprRef(originalMarkDef);
    // set orient, which can be overridden by rules as sometimes the specified orient is invalid.
    const specifiedOrient = getMarkPropOrConfig('orient', markDef, config);
    markDef.orient = orient(markDef.type, encoding, specifiedOrient);
    if (specifiedOrient !== undefined && specifiedOrient !== markDef.orient) {
        warn(orientOverridden(markDef.orient, specifiedOrient));
    }
    if (markDef.type === 'bar' && markDef.orient) {
        const cornerRadiusEnd = getMarkPropOrConfig('cornerRadiusEnd', markDef, config);
        if (cornerRadiusEnd !== undefined) {
            const newProps = (markDef.orient === 'horizontal' && encoding.x2) || (markDef.orient === 'vertical' && encoding.y2)
                ? ['cornerRadius']
                : BAR_CORNER_RADIUS_INDEX[markDef.orient];
            for (const newProp of newProps) {
                markDef[newProp] = cornerRadiusEnd;
            }
            if (markDef.cornerRadiusEnd !== undefined) {
                delete markDef.cornerRadiusEnd; // no need to keep the original cap cornerRadius
            }
        }
    }
    // set opacity and filled if not specified in mark config
    const specifiedOpacity = getMarkPropOrConfig('opacity', markDef, config);
    const specifiedFillOpacity = getMarkPropOrConfig('fillOpacity', markDef, config);
    if (specifiedOpacity === undefined && specifiedFillOpacity === undefined) {
        markDef.opacity = opacity(markDef.type, encoding);
    }
    // Set cursor, which should be pointer if href channel is present unless otherwise specified.
    // We will also set the cursor in parse via getMarkGroup since we need access to the selections.
    const specifiedCursor = getMarkPropOrConfig('cursor', markDef, config);
    if (specifiedCursor === undefined) {
        markDef.cursor = cursor(markDef, encoding, config);
    }
    return markDef;
}
function cursor(markDef, encoding, config) {
    if (encoding.href || markDef.href || getMarkPropOrConfig('href', markDef, config)) {
        return 'pointer';
    }
    return markDef.cursor;
}
const DEFAULT_REDUCED_OPACITY = 0.7;
function opacity(mark, encoding) {
    if (contains([POINT, TICK, CIRCLE, SQUARE], mark) && !isAggregate$1(encoding)) {
        return DEFAULT_REDUCED_OPACITY;
    }
    return undefined;
}
function defaultFilled(markDef, config, { graticule }) {
    if (graticule) {
        return false;
    }
    const filledConfig = getMarkConfig('filled', markDef, config);
    const mark = markDef.type;
    return getFirstDefined(filledConfig, mark !== POINT && mark !== LINE && mark !== RULE);
}
function orient(mark, encoding, specifiedOrient) {
    switch (mark) {
        case POINT:
        case CIRCLE:
        case SQUARE:
        case RECT:
        case IMAGE:
            // orient is meaningless for these marks.
            return undefined;
    }
    const { x, y, x2, y2 } = encoding;
    switch (mark) {
        case TEXT:
        case BAR:
            if (isFieldDef(x) && (isBinned(x.bin) || (isFieldDef(y) && y.aggregate && !x.aggregate))) {
                return 'vertical';
            }
            if (isFieldDef(y) && (isBinned(y.bin) || (isFieldDef(x) && x.aggregate && !y.aggregate))) {
                return 'horizontal';
            }
            if (y2 || x2) {
                // Ranged bar does not always have clear orientation, so we allow overriding
                if (specifiedOrient) {
                    return specifiedOrient;
                }
                // If y is range and x is non-range, non-bin Q
                if (!x2) {
                    if ((isFieldDef(x) && x.type === QUANTITATIVE && !isBinning(x.bin)) || isNumericDataDef(x)) {
                        if (isFieldDef(y) && isBinned(y.bin)) {
                            return 'horizontal';
                        }
                    }
                    return 'vertical';
                }
                // If x is range and y is non-range, non-bin Q
                if (!y2) {
                    if ((isFieldDef(y) && y.type === QUANTITATIVE && !isBinning(y.bin)) || isNumericDataDef(y)) {
                        if (isFieldDef(x) && isBinned(x.bin)) {
                            return 'vertical';
                        }
                    }
                    return 'horizontal';
                }
            }
        // falls through
        case RULE:
            // return undefined for line segment rule and bar with both axis ranged
            // we have to ignore the case that the data are already binned
            if (x2 && !(isFieldDef(x) && isBinned(x.bin)) && y2 && !(isFieldDef(y) && isBinned(y.bin))) {
                return undefined;
            }
        // falls through
        case AREA:
            // If there are range for both x and y, y (vertical) has higher precedence.
            if (y2) {
                if (isFieldDef(y) && isBinned(y.bin)) {
                    return 'horizontal';
                }
                else {
                    return 'vertical';
                }
            }
            else if (x2) {
                if (isFieldDef(x) && isBinned(x.bin)) {
                    return 'vertical';
                }
                else {
                    return 'horizontal';
                }
            }
            else if (mark === RULE) {
                if (x && !y) {
                    return 'vertical';
                }
                else if (y && !x) {
                    return 'horizontal';
                }
            }
        // falls through
        case LINE:
        case TICK: {
            const xIsMeasure = isUnbinnedQuantitativeFieldOrDatumDef(x);
            const yIsMeasure = isUnbinnedQuantitativeFieldOrDatumDef(y);
            if (specifiedOrient) {
                return specifiedOrient;
            }
            else if (xIsMeasure && !yIsMeasure) {
                // Tick is opposite to bar, line, area
                return mark !== 'tick' ? 'horizontal' : 'vertical';
            }
            else if (!xIsMeasure && yIsMeasure) {
                // Tick is opposite to bar, line, area
                return mark !== 'tick' ? 'vertical' : 'horizontal';
            }
            else if (xIsMeasure && yIsMeasure) {
                return 'vertical';
            }
            else {
                const xIsTemporal = isTypedFieldDef(x) && x.type === TEMPORAL;
                const yIsTemporal = isTypedFieldDef(y) && y.type === TEMPORAL;
                // x: T, y: N --> vertical tick
                if (xIsTemporal && !yIsTemporal) {
                    return 'vertical';
                }
                else if (!xIsTemporal && yIsTemporal) {
                    return 'horizontal';
                }
            }
            return undefined;
        }
    }
    return 'vertical';
}

function dropLineAndPoint(markDef) {
    const { point: _point, line: _line, ...mark } = markDef;
    return keys(mark).length > 1 ? mark : mark.type;
}
function dropLineAndPointFromConfig(config) {
    for (const mark of ['line', 'area', 'rule', 'trail']) {
        if (config[mark]) {
            config = {
                ...config,
                // TODO: remove as any
                [mark]: omit(config[mark], ['point', 'line']),
            };
        }
    }
    return config;
}
function getPointOverlay(markDef, markConfig = {}, encoding) {
    if (markDef.point === 'transparent') {
        return { opacity: 0 };
    }
    else if (markDef.point) {
        // truthy : true or object
        return isObject(markDef.point) ? markDef.point : {};
    }
    else if (markDef.point !== undefined) {
        // false or null
        return null;
    }
    else {
        // undefined (not disabled)
        if (markConfig.point || encoding.shape) {
            // enable point overlay if config[mark].point is truthy or if encoding.shape is provided
            return isObject(markConfig.point) ? markConfig.point : {};
        }
        // markDef.point is defined as falsy
        return undefined;
    }
}
function getLineOverlay(markDef, markConfig = {}) {
    if (markDef.line) {
        // true or object
        return markDef.line === true ? {} : markDef.line;
    }
    else if (markDef.line !== undefined) {
        // false or null
        return null;
    }
    else {
        // undefined (not disabled)
        if (markConfig.line) {
            // enable line overlay if config[mark].line is truthy
            return markConfig.line === true ? {} : markConfig.line;
        }
        // markDef.point is defined as falsy
        return undefined;
    }
}
class PathOverlayNormalizer {
    name = 'path-overlay';
    hasMatchingType(spec, config) {
        if (isUnitSpec(spec)) {
            const { mark, encoding } = spec;
            const markDef = isMarkDef(mark) ? mark : { type: mark };
            switch (markDef.type) {
                case 'line':
                case 'rule':
                case 'trail':
                    return !!getPointOverlay(markDef, config[markDef.type], encoding);
                case 'area':
                    return (
                    // false / null are also included as we want to remove the properties
                    !!getPointOverlay(markDef, config[markDef.type], encoding) ||
                        !!getLineOverlay(markDef, config[markDef.type]));
            }
        }
        return false;
    }
    run(spec, normParams, normalize) {
        const { config } = normParams;
        const { params, projection, mark, name, encoding: e, ...outerSpec } = spec;
        // Need to call normalizeEncoding because we need the inferred types to correctly determine stack
        const encoding = normalizeEncoding(e, config);
        const markDef = isMarkDef(mark) ? mark : { type: mark };
        const pointOverlay = getPointOverlay(markDef, config[markDef.type], encoding);
        const lineOverlay = markDef.type === 'area' && getLineOverlay(markDef, config[markDef.type]);
        const layer = [
            {
                name,
                ...(params ? { params } : {}),
                mark: dropLineAndPoint({
                    ...(markDef.type === 'area' &&
                        getMarkPropOrConfig('opacity', markDef, config) == undefined &&
                        getMarkPropOrConfig('fillOpacity', markDef, config) == undefined
                        ? { opacity: DEFAULT_REDUCED_OPACITY }
                        : {}),
                    ...markDef,
                }),
                // drop shape from encoding as this might be used to trigger point overlay
                encoding: omit(encoding, ['shape']),
            },
        ];
        // FIXME: determine rules for applying selections.
        // Need to copy stack config to overlaid layer
        // FIXME: normalizer shouldn't call `initMarkdef`, a method from an init phase.
        const stackProps = stack(initMarkdef(markDef, encoding, config), encoding);
        let overlayEncoding = encoding;
        if (stackProps) {
            const { fieldChannel: stackFieldChannel, offset } = stackProps;
            overlayEncoding = {
                ...encoding,
                [stackFieldChannel]: {
                    ...encoding[stackFieldChannel],
                    ...(offset ? { stack: offset } : {}),
                },
            };
        }
        // overlay line layer should be on the edge of area but passing y2/x2 makes
        // it as "rule" mark so that it draws unwanted vertical/horizontal lines.
        // point overlay also should not have y2/x2 as it does not support.
        overlayEncoding = omit(overlayEncoding, ['y2', 'x2']);
        if (lineOverlay) {
            layer.push({
                ...(projection ? { projection } : {}),
                mark: {
                    type: 'line',
                    ...pick(markDef, ['clip', 'interpolate', 'tension', 'tooltip']),
                    ...lineOverlay,
                },
                encoding: overlayEncoding,
            });
        }
        if (pointOverlay) {
            layer.push({
                ...(projection ? { projection } : {}),
                mark: {
                    type: 'point',
                    opacity: 1,
                    filled: true,
                    ...pick(markDef, ['clip', 'tooltip']),
                    ...pointOverlay,
                },
                encoding: overlayEncoding,
            });
        }
        return normalize({
            ...outerSpec,
            layer,
        }, {
            ...normParams,
            config: dropLineAndPointFromConfig(config),
        });
    }
}

function replaceRepeaterInFacet(facet, repeater) {
    if (!repeater) {
        return facet;
    }
    if (isFacetMapping(facet)) {
        return replaceRepeaterInMapping(facet, repeater);
    }
    return replaceRepeaterInFieldDef(facet, repeater);
}
function replaceRepeaterInEncoding(encoding, repeater) {
    if (!repeater) {
        return encoding;
    }
    return replaceRepeaterInMapping(encoding, repeater);
}
/**
 * Replaces repeated value and returns if the repeated value is valid.
 */
function replaceRepeatInProp(prop, o, repeater) {
    const val = o[prop];
    if (isRepeatRef(val)) {
        if (val.repeat in repeater) {
            return { ...o, [prop]: repeater[val.repeat] };
        }
        else {
            warn(noSuchRepeatedValue(val.repeat));
            return undefined;
        }
    }
    return o;
}
/**
 * Replace repeater values in a field def with the concrete field name.
 */
function replaceRepeaterInFieldDef(fieldDef, repeater) {
    fieldDef = replaceRepeatInProp('field', fieldDef, repeater);
    if (fieldDef === undefined) {
        // the field def should be ignored
        return undefined;
    }
    else if (fieldDef === null) {
        return null;
    }
    if (isSortableFieldDef(fieldDef) && isSortField(fieldDef.sort)) {
        const sort = replaceRepeatInProp('field', fieldDef.sort, repeater);
        fieldDef = {
            ...fieldDef,
            ...(sort ? { sort } : {}),
        };
    }
    return fieldDef;
}
function replaceRepeaterInFieldOrDatumDef(def, repeater) {
    if (isFieldDef(def)) {
        return replaceRepeaterInFieldDef(def, repeater);
    }
    else {
        const datumDef = replaceRepeatInProp('datum', def, repeater);
        if (datumDef !== def && !datumDef.type) {
            datumDef.type = 'nominal';
        }
        return datumDef;
    }
}
function replaceRepeaterInChannelDef(channelDef, repeater) {
    if (isFieldOrDatumDef(channelDef)) {
        const fd = replaceRepeaterInFieldOrDatumDef(channelDef, repeater);
        if (fd) {
            return fd;
        }
        else if (isConditionalDef(channelDef)) {
            return { condition: channelDef.condition };
        }
    }
    else {
        if (hasConditionalFieldOrDatumDef(channelDef)) {
            const fd = replaceRepeaterInFieldOrDatumDef(channelDef.condition, repeater);
            if (fd) {
                return {
                    ...channelDef,
                    condition: fd,
                };
            }
            else {
                const { condition, ...channelDefWithoutCondition } = channelDef;
                return channelDefWithoutCondition;
            }
        }
        return channelDef;
    }
    return undefined;
}
function replaceRepeaterInMapping(mapping, repeater) {
    const out = {};
    for (const channel in mapping) {
        if (hasProperty(mapping, channel)) {
            const channelDef = mapping[channel];
            if (isArray(channelDef)) {
                // array cannot have condition
                out[channel] = channelDef // somehow we need to cast it here
                    .map((cd) => replaceRepeaterInChannelDef(cd, repeater))
                    .filter((cd) => cd);
            }
            else {
                const cd = replaceRepeaterInChannelDef(channelDef, repeater);
                if (cd !== undefined) {
                    out[channel] = cd;
                }
            }
        }
    }
    return out;
}

class RuleForRangedLineNormalizer {
    name = 'RuleForRangedLine';
    hasMatchingType(spec) {
        if (isUnitSpec(spec)) {
            const { encoding, mark } = spec;
            if (mark === 'line' || (isMarkDef(mark) && mark.type === 'line')) {
                for (const channel of SECONDARY_RANGE_CHANNEL) {
                    const mainChannel = getMainRangeChannel(channel);
                    const mainChannelDef = encoding[mainChannel];
                    if (encoding[channel]) {
                        if ((isFieldDef(mainChannelDef) && !isBinned(mainChannelDef.bin)) || isDatumDef(mainChannelDef)) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
    run(spec, params, normalize) {
        const { encoding, mark } = spec;
        warn(lineWithRange(!!encoding.x2, !!encoding.y2));
        return normalize({
            ...spec,
            mark: isObject(mark) ? { ...mark, type: 'rule' } : 'rule',
        }, params);
    }
}

class CoreNormalizer extends SpecMapper {
    nonFacetUnitNormalizers = [
        boxPlotNormalizer,
        errorBarNormalizer,
        errorBandNormalizer,
        new PathOverlayNormalizer(),
        new RuleForRangedLineNormalizer(),
    ];
    map(spec, params) {
        // Special handling for a faceted unit spec as it can return a facet spec, not just a layer or unit spec like a normal unit spec.
        if (isUnitSpec(spec)) {
            const hasRow = channelHasField(spec.encoding, ROW);
            const hasColumn = channelHasField(spec.encoding, COLUMN);
            const hasFacet = channelHasField(spec.encoding, FACET);
            if (hasRow || hasColumn || hasFacet) {
                return this.mapFacetedUnit(spec, params);
            }
        }
        return super.map(spec, params);
    }
    // This is for normalizing non-facet unit
    mapUnit(spec, params) {
        const { parentEncoding, parentProjection } = params;
        const encoding = replaceRepeaterInEncoding(spec.encoding, params.repeater);
        const specWithReplacedEncoding = {
            ...spec,
            ...(spec.name ? { name: [params.repeaterPrefix, spec.name].filter((n) => n).join('_') } : {}),
            ...(encoding ? { encoding } : {}),
        };
        if (parentEncoding || parentProjection) {
            return this.mapUnitWithParentEncodingOrProjection(specWithReplacedEncoding, params);
        }
        const normalizeLayerOrUnit = this.mapLayerOrUnit.bind(this);
        for (const unitNormalizer of this.nonFacetUnitNormalizers) {
            if (unitNormalizer.hasMatchingType(specWithReplacedEncoding, params.config)) {
                return unitNormalizer.run(specWithReplacedEncoding, params, normalizeLayerOrUnit);
            }
        }
        return specWithReplacedEncoding;
    }
    mapRepeat(spec, params) {
        if (isLayerRepeatSpec(spec)) {
            return this.mapLayerRepeat(spec, params);
        }
        else {
            return this.mapNonLayerRepeat(spec, params);
        }
    }
    mapLayerRepeat(spec, params) {
        const { repeat, spec: childSpec, ...rest } = spec;
        const { row, column, layer } = repeat;
        const { repeater = {}, repeaterPrefix = '' } = params;
        if (row || column) {
            return this.mapRepeat({
                ...spec,
                repeat: {
                    ...(row ? { row } : {}),
                    ...(column ? { column } : {}),
                },
                spec: {
                    repeat: { layer },
                    spec: childSpec,
                },
            }, params);
        }
        else {
            return {
                ...rest,
                layer: layer.map((layerValue) => {
                    const childRepeater = {
                        ...repeater,
                        layer: layerValue,
                    };
                    const childName = `${(childSpec.name ? `${childSpec.name}_` : '') + repeaterPrefix}child__layer_${varName(layerValue)}`;
                    const child = this.mapLayerOrUnit(childSpec, { ...params, repeater: childRepeater, repeaterPrefix: childName });
                    child.name = childName;
                    return child;
                }),
            };
        }
    }
    mapNonLayerRepeat(spec, params) {
        const { repeat, spec: childSpec, data, ...remainingProperties } = spec;
        if (!isArray(repeat) && spec.columns) {
            // is repeat with row/column
            spec = omit(spec, ['columns']);
            warn(columnsNotSupportByRowCol('repeat'));
        }
        const concat = [];
        const { repeater = {}, repeaterPrefix = '' } = params;
        const row = (!isArray(repeat) && repeat.row) || [repeater ? repeater.row : null];
        const column = (!isArray(repeat) && repeat.column) || [repeater ? repeater.column : null];
        const repeatValues = (isArray(repeat) && repeat) || [repeater ? repeater.repeat : null];
        // cross product
        for (const repeatValue of repeatValues) {
            for (const rowValue of row) {
                for (const columnValue of column) {
                    const childRepeater = {
                        repeat: repeatValue,
                        row: rowValue,
                        column: columnValue,
                        layer: repeater.layer,
                    };
                    const childName = `${(childSpec.name ? `${childSpec.name}_` : '') + repeaterPrefix}child__${isArray(repeat)
                        ? `${varName(repeatValue)}`
                        : (repeat.row ? `row_${varName(rowValue)}` : '') + (repeat.column ? `column_${varName(columnValue)}` : '')}`;
                    const child = this.map(childSpec, { ...params, repeater: childRepeater, repeaterPrefix: childName });
                    child.name = childName;
                    // we move data up
                    concat.push(omit(child, ['data']));
                }
            }
        }
        const columns = isArray(repeat) ? spec.columns : repeat.column ? repeat.column.length : 1;
        return {
            data: childSpec.data ?? data, // data from child spec should have precedence
            align: 'all',
            ...remainingProperties,
            columns,
            concat,
        };
    }
    mapFacet(spec, params) {
        const { facet } = spec;
        if (isFacetMapping(facet) && spec.columns) {
            // is facet with row/column
            spec = omit(spec, ['columns']);
            warn(columnsNotSupportByRowCol('facet'));
        }
        return super.mapFacet(spec, params);
    }
    mapUnitWithParentEncodingOrProjection(spec, params) {
        const { encoding, projection } = spec;
        const { parentEncoding, parentProjection, config } = params;
        const mergedProjection = mergeProjection({ parentProjection, projection });
        const mergedEncoding = mergeEncoding({
            parentEncoding,
            encoding: replaceRepeaterInEncoding(encoding, params.repeater),
        });
        return this.mapUnit({
            ...spec,
            ...(mergedProjection ? { projection: mergedProjection } : {}),
            ...(mergedEncoding ? { encoding: mergedEncoding } : {}),
        }, { config });
    }
    mapFacetedUnit(spec, normParams) {
        // New encoding in the inside spec should not contain row / column
        // as row/column should be moved to facet
        const { row, column, facet, ...encoding } = spec.encoding;
        // Mark and encoding should be moved into the inner spec
        const { mark, width, projection, height, view, params, encoding: _, ...outerSpec } = spec;
        const { facetMapping, layout } = this.getFacetMappingAndLayout({ row, column, facet }, normParams);
        const newEncoding = replaceRepeaterInEncoding(encoding, normParams.repeater);
        return this.mapFacet({
            ...outerSpec,
            ...layout,
            // row / column has higher precedence than facet
            facet: facetMapping,
            spec: {
                ...(width ? { width } : {}),
                ...(height ? { height } : {}),
                ...(view ? { view } : {}),
                ...(projection ? { projection } : {}),
                mark,
                encoding: newEncoding,
                ...(params ? { params } : {}),
            },
        }, normParams);
    }
    getFacetMappingAndLayout(facets, params) {
        const { row, column, facet } = facets;
        if (row || column) {
            if (facet) {
                warn(facetChannelDropped([...(row ? [ROW] : []), ...(column ? [COLUMN] : [])]));
            }
            const facetMapping = {};
            const layout = {};
            for (const channel of [ROW, COLUMN]) {
                const def = facets[channel];
                if (def) {
                    const { align, center, spacing, columns, ...defWithoutLayout } = def;
                    facetMapping[channel] = defWithoutLayout;
                    for (const prop of ['align', 'center', 'spacing']) {
                        if (def[prop] !== undefined) {
                            layout[prop] ??= {};
                            layout[prop][channel] = def[prop];
                        }
                    }
                }
            }
            return { facetMapping, layout };
        }
        else {
            const { align, center, spacing, columns, ...facetMapping } = facet;
            return {
                facetMapping: replaceRepeaterInFacet(facetMapping, params.repeater),
                layout: {
                    ...(align ? { align } : {}),
                    ...(center ? { center } : {}),
                    ...(spacing ? { spacing } : {}),
                    ...(columns ? { columns } : {}),
                },
            };
        }
    }
    mapLayer(spec, { parentEncoding, parentProjection, ...otherParams }) {
        // Special handling for extended layer spec
        const { encoding, projection, ...rest } = spec;
        const params = {
            ...otherParams,
            parentEncoding: mergeEncoding({ parentEncoding, encoding, layer: true }),
            parentProjection: mergeProjection({ parentProjection, projection }),
        };
        return super.mapLayer({
            ...rest,
            ...(spec.name ? { name: [params.repeaterPrefix, spec.name].filter((n) => n).join('_') } : {}),
        }, params);
    }
}
function mergeEncoding({ parentEncoding, encoding = {}, layer, }) {
    let merged = {};
    if (parentEncoding) {
        const channels = new Set([...keys(parentEncoding), ...keys(encoding)]);
        for (const channel of channels) {
            const channelDef = encoding[channel];
            const parentChannelDef = parentEncoding[channel];
            if (isFieldOrDatumDef(channelDef)) {
                // Field/Datum Def can inherit properties from its parent
                // Note that parentChannelDef doesn't have to be a field/datum def if the channelDef is already one.
                const mergedChannelDef = {
                    ...parentChannelDef,
                    ...channelDef,
                };
                merged[channel] = mergedChannelDef;
            }
            else if (hasConditionalFieldOrDatumDef(channelDef)) {
                merged[channel] = {
                    ...channelDef,
                    condition: {
                        ...parentChannelDef,
                        ...channelDef.condition,
                    },
                };
            }
            else if (channelDef || channelDef === null) {
                merged[channel] = channelDef;
            }
            else if (layer ||
                isValueDef(parentChannelDef) ||
                isSignalRef(parentChannelDef) ||
                isFieldOrDatumDef(parentChannelDef) ||
                isArray(parentChannelDef)) {
                merged[channel] = parentChannelDef;
            }
        }
    }
    else {
        merged = encoding;
    }
    return !merged || isEmpty(merged) ? undefined : merged;
}
function mergeProjection(opt) {
    const { parentProjection, projection } = opt;
    if (parentProjection && projection) {
        warn(projectionOverridden({ parentProjection, projection }));
    }
    return projection ?? parentProjection;
}

function isFilter(t) {
    return hasProperty(t, 'filter');
}
function isImputeSequence(t) {
    return hasProperty(t, 'stop');
}
function isLookup(t) {
    return hasProperty(t, 'lookup');
}
function isLookupData(from) {
    return hasProperty(from, 'data');
}
function isLookupSelection(from) {
    return hasProperty(from, 'param');
}
function isPivot(t) {
    return hasProperty(t, 'pivot');
}
function isDensity(t) {
    return hasProperty(t, 'density');
}
function isQuantile(t) {
    return hasProperty(t, 'quantile');
}
function isRegression(t) {
    return hasProperty(t, 'regression');
}
function isLoess(t) {
    return hasProperty(t, 'loess');
}
function isSample(t) {
    return hasProperty(t, 'sample');
}
function isWindow(t) {
    return hasProperty(t, 'window');
}
function isJoinAggregate(t) {
    return hasProperty(t, 'joinaggregate');
}
function isFlatten(t) {
    return hasProperty(t, 'flatten');
}
function isCalculate(t) {
    return hasProperty(t, 'calculate');
}
function isBin(t) {
    return hasProperty(t, 'bin');
}
function isImpute(t) {
    return hasProperty(t, 'impute');
}
function isTimeUnit(t) {
    return hasProperty(t, 'timeUnit');
}
function isAggregate(t) {
    return hasProperty(t, 'aggregate');
}
function isStack(t) {
    return hasProperty(t, 'stack');
}
function isFold(t) {
    return hasProperty(t, 'fold');
}
function isExtent(t) {
    return hasProperty(t, 'extent') && !hasProperty(t, 'density') && !hasProperty(t, 'regression');
}
function normalizeTransform(transform) {
    return transform.map((t) => {
        if (isFilter(t)) {
            return {
                filter: normalizeLogicalComposition(t.filter, normalizePredicate$1),
            };
        }
        return t;
    });
}

class SelectionCompatibilityNormalizer extends SpecMapper {
    map(spec, normParams) {
        normParams.emptySelections ??= {};
        normParams.selectionPredicates ??= {};
        spec = normalizeTransforms(spec, normParams);
        return super.map(spec, normParams);
    }
    mapLayerOrUnit(spec, normParams) {
        spec = normalizeTransforms(spec, normParams);
        if (spec.encoding) {
            const encoding = {};
            for (const [channel, enc] of entries$1(spec.encoding)) {
                encoding[channel] = normalizeChannelDef(enc, normParams);
            }
            spec = { ...spec, encoding };
        }
        return super.mapLayerOrUnit(spec, normParams);
    }
    mapUnit(spec, normParams) {
        const { selection, ...rest } = spec;
        if (selection) {
            return {
                ...rest,
                params: entries$1(selection).map(([name, selDef]) => {
                    const { init: value, bind, empty, ...select } = selDef;
                    if (select.type === 'single') {
                        select.type = 'point';
                        select.toggle = false;
                    }
                    else if (select.type === 'multi') {
                        select.type = 'point';
                    }
                    // Propagate emptiness forwards and backwards
                    normParams.emptySelections[name] = empty !== 'none';
                    for (const pred of vals(normParams.selectionPredicates[name] ?? {})) {
                        pred.empty = empty !== 'none';
                    }
                    return { name, value, select, bind };
                }),
            };
        }
        return spec;
    }
}
function normalizeTransforms(spec, normParams) {
    const { transform: tx, ...rest } = spec;
    if (tx) {
        const transform = tx.map((t) => {
            if (isFilter(t)) {
                return { filter: normalizePredicate(t, normParams) };
            }
            else if (isBin(t) && isBinParams(t.bin)) {
                return {
                    ...t,
                    bin: normalizeBinExtent(t.bin),
                };
            }
            else if (isLookup(t)) {
                const { selection: param, ...from } = t.from;
                return param
                    ? {
                        ...t,
                        from: { param, ...from },
                    }
                    : t;
            }
            return t;
        });
        return { ...rest, transform };
    }
    return spec;
}
function normalizeChannelDef(obj, normParams) {
    const enc = duplicate(obj);
    if (isFieldDef(enc) && isBinParams(enc.bin)) {
        enc.bin = normalizeBinExtent(enc.bin);
    }
    if (isScaleFieldDef(enc) && enc.scale?.domain?.selection) {
        const { selection: param, ...domain } = enc.scale.domain;
        enc.scale.domain = { ...domain, ...(param ? { param } : {}) };
    }
    if (isConditionalDef(enc)) {
        if (isArray(enc.condition)) {
            enc.condition = enc.condition.map((c) => {
                const { selection, param, test, ...cond } = c;
                return param ? c : { ...cond, test: normalizePredicate(c, normParams) };
            });
        }
        else {
            const { selection, param, test, ...cond } = normalizeChannelDef(enc.condition, normParams);
            enc.condition = param
                ? enc.condition
                : {
                    ...cond,
                    test: normalizePredicate(enc.condition, normParams),
                };
        }
    }
    return enc;
}
function normalizeBinExtent(bin) {
    const ext = bin.extent;
    if (ext?.selection) {
        const { selection: param, ...rest } = ext;
        return { ...bin, extent: { ...rest, param } };
    }
    return bin;
}
function normalizePredicate(op, normParams) {
    // Normalize old compositions of selection names (e.g., selection: {and: ["one", "two"]})
    const normalizeSelectionComposition = (o) => {
        return normalizeLogicalComposition(o, (param) => {
            const empty = normParams.emptySelections[param] ?? true;
            const pred = { param, empty };
            normParams.selectionPredicates[param] ??= [];
            normParams.selectionPredicates[param].push(pred);
            return pred;
        });
    };
    return op.selection
        ? normalizeSelectionComposition(op.selection)
        : normalizeLogicalComposition(op.test || op.filter, (o) => o.selection ? normalizeSelectionComposition(o.selection) : o);
}

class TopLevelSelectionsNormalizer extends SpecMapper {
    map(spec, normParams) {
        const selections = normParams.selections ?? [];
        if (spec.params && !isUnitSpec(spec)) {
            const params = [];
            for (const param of spec.params) {
                if (isSelectionParameter(param)) {
                    selections.push(param);
                }
                else {
                    params.push(param);
                }
            }
            spec.params = params;
        }
        normParams.selections = selections;
        return super.map(spec, normParams);
    }
    mapUnit(spec, normParams) {
        const selections = normParams.selections;
        if (!selections || !selections.length)
            return spec;
        const path = (normParams.path ?? []).concat(spec.name);
        const params = [];
        for (const selection of selections) {
            // By default, apply selections to all unit views.
            if (!selection.views || !selection.views.length) {
                params.push(selection);
            }
            else {
                for (const view of selection.views) {
                    // view is either a specific unit name, or a partial path through the spec tree.
                    if ((isString(view) && (view === spec.name || path.includes(view))) ||
                        (isArray(view) &&
                            // logic for backwards compatibility with view paths before we had unique names
                            view.map((v) => path.indexOf(v)).every((v, i, arr) => v !== -1 && (i === 0 || v > arr[i - 1])))) {
                        params.push(selection);
                    }
                }
            }
        }
        if (params.length)
            spec.params = params;
        return spec;
    }
}
for (const method of ['mapFacet', 'mapRepeat', 'mapHConcat', 'mapVConcat', 'mapLayer']) {
    const proto = TopLevelSelectionsNormalizer.prototype[method];
    TopLevelSelectionsNormalizer.prototype[method] = function (spec, params) {
        return proto.call(this, spec, addSpecNameToParams(spec, params));
    };
}
function addSpecNameToParams(spec, params) {
    return spec.name
        ? {
            ...params,
            path: (params.path ?? []).concat(spec.name),
        }
        : params;
}

function normalize(spec, config) {
    if (config === undefined) {
        config = initConfig(spec.config);
    }
    const normalizedSpec = normalizeGenericSpec(spec, config);
    const { width, height } = spec;
    const autosize = normalizeAutoSize(normalizedSpec, { width, height, autosize: spec.autosize }, config);
    return {
        ...normalizedSpec,
        ...(autosize ? { autosize } : {}),
    };
}
const coreNormalizer = new CoreNormalizer();
const selectionCompatNormalizer = new SelectionCompatibilityNormalizer();
const topLevelSelectionNormalizer = new TopLevelSelectionsNormalizer();
/**
 * Decompose extended unit specs into composition of pure unit specs.
 * And push top-level selection definitions down to unit specs.
 */
function normalizeGenericSpec(spec, config = {}) {
    const normParams = { config };
    return topLevelSelectionNormalizer.map(coreNormalizer.map(selectionCompatNormalizer.map(spec, normParams), normParams), normParams);
}
function _normalizeAutoSize(autosize) {
    return isString(autosize) ? { type: autosize } : (autosize ?? {});
}
/**
 * Normalize autosize and deal with width or height == "container".
 */
function normalizeAutoSize(spec, sizeInfo, config) {
    let { width, height } = sizeInfo;
    const isFitCompatible = isUnitSpec(spec) || isLayerSpec(spec);
    const autosizeDefault = {};
    if (!isFitCompatible) {
        // If spec is not compatible with autosize == "fit", discard width/height == container
        if (width == 'container') {
            warn(containerSizeNonSingle('width'));
            width = undefined;
        }
        if (height == 'container') {
            warn(containerSizeNonSingle('height'));
            height = undefined;
        }
    }
    else {
        // Default autosize parameters to fit when width/height is "container"
        if (width == 'container' && height == 'container') {
            autosizeDefault.type = 'fit';
            autosizeDefault.contains = 'padding';
        }
        else if (width == 'container') {
            autosizeDefault.type = 'fit-x';
            autosizeDefault.contains = 'padding';
        }
        else if (height == 'container') {
            autosizeDefault.type = 'fit-y';
            autosizeDefault.contains = 'padding';
        }
    }
    const autosize = {
        type: 'pad',
        ...autosizeDefault,
        ...(config ? _normalizeAutoSize(config.autosize) : {}),
        ..._normalizeAutoSize(spec.autosize),
    };
    if (autosize.type === 'fit' && !isFitCompatible) {
        warn(FIT_NON_SINGLE);
        autosize.type = 'pad';
    }
    if (width == 'container' && !(autosize.type == 'fit' || autosize.type == 'fit-x')) {
        warn(containerSizeNotCompatibleWithAutosize('width'));
    }
    if (height == 'container' && !(autosize.type == 'fit' || autosize.type == 'fit-y')) {
        warn(containerSizeNotCompatibleWithAutosize('height'));
    }
    // Delete autosize property if it's Vega's default
    if (deepEqual(autosize, { type: 'pad' })) {
        return undefined;
    }
    return autosize;
}

function isFitType(autoSizeType) {
    return ['fit', 'fit-x', 'fit-y'].includes(autoSizeType);
}
function getFitType(sizeType) {
    return sizeType ? `fit-${getPositionScaleChannel(sizeType)}` : 'fit';
}
const TOP_LEVEL_PROPERTIES = [
    'background',
    'padding',
    // We do not include "autosize" here as it is supported by only unit and layer specs and thus need to be normalized
];
function extractTopLevelProperties(t, includeParams) {
    const o = {};
    for (const p of TOP_LEVEL_PROPERTIES) {
        if (t && t[p] !== undefined) {
            o[p] = signalRefOrValue(t[p]);
        }
    }
    if (includeParams) {
        o.params = t.params;
    }
    return o;
}

/**
 * Generic class for storing properties that are explicitly specified
 * and implicitly determined by the compiler.
 * This is important for scale/axis/legend merging as
 * we want to prioritize properties that users explicitly specified.
 */
class Split {
    explicit;
    implicit;
    constructor(explicit = {}, implicit = {}) {
        this.explicit = explicit;
        this.implicit = implicit;
    }
    clone() {
        return new Split(duplicate(this.explicit), duplicate(this.implicit));
    }
    combine() {
        return {
            ...this.explicit, // Explicit properties comes first
            ...this.implicit,
        };
    }
    get(key) {
        // Explicit has higher precedence
        return getFirstDefined(this.explicit[key], this.implicit[key]);
    }
    getWithExplicit(key) {
        // Explicit has higher precedence
        if (this.explicit[key] !== undefined) {
            return { explicit: true, value: this.explicit[key] };
        }
        else if (this.implicit[key] !== undefined) {
            return { explicit: false, value: this.implicit[key] };
        }
        return { explicit: false, value: undefined };
    }
    setWithExplicit(key, { value, explicit }) {
        if (value !== undefined) {
            this.set(key, value, explicit);
        }
    }
    set(key, value, explicit) {
        delete this[explicit ? 'implicit' : 'explicit'][key];
        this[explicit ? 'explicit' : 'implicit'][key] = value;
        return this;
    }
    copyKeyFromSplit(key, { explicit, implicit }) {
        // Explicit has higher precedence
        if (explicit[key] !== undefined) {
            this.set(key, explicit[key], true);
        }
        else if (implicit[key] !== undefined) {
            this.set(key, implicit[key], false);
        }
    }
    copyKeyFromObject(key, s) {
        // Explicit has higher precedence
        if (s[key] !== undefined) {
            this.set(key, s[key], true);
        }
    }
    /**
     * Merge split object into this split object. Properties from the other split
     * overwrite properties from this split.
     */
    copyAll(other) {
        for (const key of keys(other.combine())) {
            const val = other.getWithExplicit(key);
            this.setWithExplicit(key, val);
        }
    }
}
function makeExplicit(value) {
    return {
        explicit: true,
        value,
    };
}
function makeImplicit(value) {
    return {
        explicit: false,
        value,
    };
}
function tieBreakByComparing(compare) {
    return (v1, v2, property, propertyOf) => {
        const diff = compare(v1.value, v2.value);
        if (diff > 0) {
            return v1;
        }
        else if (diff < 0) {
            return v2;
        }
        return defaultTieBreaker(v1, v2, property, propertyOf);
    };
}
function defaultTieBreaker(v1, v2, property, propertyOf) {
    if (v1.explicit && v2.explicit) {
        warn(mergeConflictingProperty(property, propertyOf, v1.value, v2.value));
    }
    // If equal score, prefer v1.
    return v1;
}
function mergeValuesWithExplicit(v1, v2, property, propertyOf, tieBreaker = defaultTieBreaker) {
    if (v1 === undefined || v1.value === undefined) {
        // For first run
        return v2;
    }
    if (v1.explicit && !v2.explicit) {
        return v1;
    }
    else if (v2.explicit && !v1.explicit) {
        return v2;
    }
    else if (deepEqual(v1.value, v2.value)) {
        return v1;
    }
    else {
        return tieBreaker(v1, v2, property, propertyOf);
    }
}

/**
 * Class to track interesting properties (see https://15721.courses.cs.cmu.edu/spring2016/papers/graefe-ieee1995.pdf)
 * about how fields have been parsed or whether they have been derived in a transform. We use this to not parse the
 * same field again (or differently).
 */
class AncestorParse extends Split {
    explicit;
    implicit;
    parseNothing;
    constructor(explicit = {}, implicit = {}, parseNothing = false) {
        super(explicit, implicit);
        this.explicit = explicit;
        this.implicit = implicit;
        this.parseNothing = parseNothing;
    }
    clone() {
        const clone = super.clone();
        clone.parseNothing = this.parseNothing;
        return clone;
    }
}

function isUrlData(data) {
    return hasProperty(data, 'url');
}
function isInlineData(data) {
    return hasProperty(data, 'values');
}
function isNamedData(data) {
    return hasProperty(data, 'name') && !isUrlData(data) && !isInlineData(data) && !isGenerator(data);
}
function isGenerator(data) {
    return data && (isSequenceGenerator(data) || isSphereGenerator(data) || isGraticuleGenerator(data));
}
function isSequenceGenerator(data) {
    return hasProperty(data, 'sequence');
}
function isSphereGenerator(data) {
    return hasProperty(data, 'sphere');
}
function isGraticuleGenerator(data) {
    return hasProperty(data, 'graticule');
}
var DataSourceType;
(function (DataSourceType) {
    DataSourceType[DataSourceType["Raw"] = 0] = "Raw";
    /** Main data source for marks */
    DataSourceType[DataSourceType["Main"] = 1] = "Main";
    DataSourceType[DataSourceType["Row"] = 2] = "Row";
    DataSourceType[DataSourceType["Column"] = 3] = "Column";
    DataSourceType[DataSourceType["Lookup"] = 4] = "Lookup";
    /** Pre-filter-invalid data source for scale domains */
    DataSourceType[DataSourceType["PreFilterInvalid"] = 5] = "PreFilterInvalid";
    /** Post-filter-invalid data source for scale domains */
    DataSourceType[DataSourceType["PostFilterInvalid"] = 6] = "PostFilterInvalid";
})(DataSourceType || (DataSourceType = {}));

function getDataSourcesForHandlingInvalidValues({ invalid, isPath, }) {
    const normalizedInvalid = normalizeInvalidDataMode(invalid, { isPath });
    switch (normalizedInvalid) {
        case 'filter':
            // Both marks and scales use post-filter data
            return {
                marks: 'exclude-invalid-values',
                scales: 'exclude-invalid-values',
            };
        case 'break-paths-show-domains':
            return {
                // Path-based marks use pre-filter data so we know to skip these invalid points in the path.
                // For non-path based marks, we skip by not showing them at all.
                marks: isPath ? 'include-invalid-values' : 'exclude-invalid-values',
                scales: 'include-invalid-values',
            };
        case 'break-paths-filter-domains':
            // For path marks, the marks will use unfiltered data (and skip points). But we need a separate data sources to feed the domain.
            // For non-path marks, we can use the filtered data for both marks and scales.
            return {
                marks: isPath ? 'include-invalid-values' : 'exclude-invalid-values',
                // Unlike 'break-paths-show-domains', 'break-paths-filter-domains' uses post-filter data to feed scale.
                scales: 'exclude-invalid-values',
            };
        case 'show':
            return {
                marks: 'include-invalid-values',
                scales: 'include-invalid-values',
            };
    }
}
function getScaleDataSourceForHandlingInvalidValues(props) {
    const { marks, scales } = getDataSourcesForHandlingInvalidValues(props);
    if (marks === scales) {
        // If both marks and scales use the same data, there is only the main data source.
        return DataSourceType.Main;
    }
    // If marks and scales use differetnt data, return the pre/post-filter data source accordingly.
    return scales === 'include-invalid-values' ? DataSourceType.PreFilterInvalid : DataSourceType.PostFilterInvalid;
}

/**
 * A node in the dataflow tree.
 */
class DataFlowNode {
    debugName;
    _children = [];
    _parent = null;
    _hash;
    constructor(parent, debugName) {
        this.debugName = debugName;
        if (parent) {
            this.parent = parent;
        }
    }
    /**
     * Clone this node with a deep copy but don't clone links to children or parents.
     */
    clone() {
        throw new Error('Cannot clone node');
    }
    get parent() {
        return this._parent;
    }
    /**
     * Set the parent of the node and also add this node to the parent's children.
     */
    set parent(parent) {
        this._parent = parent;
        if (parent) {
            parent.addChild(this);
        }
    }
    get children() {
        return this._children;
    }
    numChildren() {
        return this._children.length;
    }
    addChild(child, loc) {
        // do not add the same child twice
        if (this._children.includes(child)) {
            warn(ADD_SAME_CHILD_TWICE);
            return;
        }
        if (loc !== undefined) {
            this._children.splice(loc, 0, child);
        }
        else {
            this._children.push(child);
        }
    }
    removeChild(oldChild) {
        const loc = this._children.indexOf(oldChild);
        this._children.splice(loc, 1);
        return loc;
    }
    /**
     * Remove node from the dataflow.
     */
    remove() {
        let loc = this._parent.removeChild(this);
        for (const child of this._children) {
            // do not use the set method because we want to insert at a particular location
            child._parent = this._parent;
            this._parent.addChild(child, loc++);
        }
    }
    /**
     * Insert another node as a parent of this node.
     */
    insertAsParentOf(other) {
        const parent = other.parent;
        parent.removeChild(this);
        this.parent = parent;
        other.parent = this;
    }
    swapWithParent() {
        const parent = this._parent;
        const newParent = parent.parent;
        // reconnect the children
        for (const child of this._children) {
            child.parent = parent;
        }
        // remove old links
        this._children = []; // equivalent to removing every child link one by one
        parent.removeChild(this);
        const loc = parent.parent.removeChild(parent);
        // swap two nodes but maintain order in children
        this._parent = newParent;
        newParent.addChild(this, loc);
        parent.parent = this;
    }
}
class OutputNode extends DataFlowNode {
    type;
    refCounts;
    _source;
    _name;
    clone() {
        const cloneObj = new this.constructor();
        cloneObj.debugName = `clone_${this.debugName}`;
        cloneObj._source = this._source;
        cloneObj._name = `clone_${this._name}`;
        cloneObj.type = this.type;
        cloneObj.refCounts = this.refCounts;
        cloneObj.refCounts[cloneObj._name] = 0;
        return cloneObj;
    }
    /**
     * @param source The name of the source. Will change in assemble.
     * @param type The type of the output node.
     * @param refCounts A global ref counter map.
     */
    constructor(parent, source, type, refCounts) {
        super(parent, source);
        this.type = type;
        this.refCounts = refCounts;
        this._source = this._name = source;
        if (this.refCounts && !(this._name in this.refCounts)) {
            this.refCounts[this._name] = 0;
        }
    }
    dependentFields() {
        return new Set();
    }
    producedFields() {
        return new Set();
    }
    hash() {
        if (this._hash === undefined) {
            this._hash = `Output ${uniqueId()}`;
        }
        return this._hash;
    }
    /**
     * Request the datasource name and increase the ref counter.
     *
     * During the parsing phase, this will return the simple name such as 'main' or 'raw'.
     * It is crucial to request the name from an output node to mark it as a required node.
     * If nobody ever requests the name, this datasource will not be instantiated in the assemble phase.
     *
     * In the assemble phase, this will return the correct name.
     */
    getSource() {
        this.refCounts[this._name]++;
        return this._source;
    }
    isRequired() {
        return !!this.refCounts[this._name];
    }
    setSource(source) {
        this._source = source;
    }
}

function isTimeUnitTransformComponent(timeUnitComponent) {
    return timeUnitComponent.as !== undefined;
}
function offsetAs(field) {
    return `${field}_end`;
}
class TimeUnitNode extends DataFlowNode {
    timeUnits;
    clone() {
        return new TimeUnitNode(null, duplicate(this.timeUnits));
    }
    constructor(parent, timeUnits) {
        super(parent);
        this.timeUnits = timeUnits;
    }
    static makeFromEncoding(parent, model) {
        const formula = model.reduceFieldDef((timeUnitComponent, fieldDef, channel) => {
            const { field, timeUnit } = fieldDef;
            if (timeUnit) {
                let component;
                if (isBinnedTimeUnit(timeUnit)) {
                    // For binned time unit, only produce end if the mark is a rect-based mark (rect, bar, image, arc), which needs "range".
                    if (isUnitModel(model)) {
                        const { mark, markDef, config } = model;
                        const bandPosition = getBandPosition({ fieldDef, markDef, config });
                        if (isRectBasedMark(mark) || !!bandPosition) {
                            component = {
                                timeUnit: normalizeTimeUnit(timeUnit),
                                field,
                            };
                        }
                    }
                }
                else {
                    component = {
                        as: vgField(fieldDef, { forAs: true }),
                        field,
                        timeUnit,
                    };
                }
                if (isUnitModel(model)) {
                    const { mark, markDef, config } = model;
                    const bandPosition = getBandPosition({ fieldDef, markDef, config });
                    if (isRectBasedMark(mark) && isXorY(channel) && bandPosition !== 0.5) {
                        component.rectBandPosition = bandPosition;
                    }
                }
                if (component) {
                    timeUnitComponent[hash(component)] = component;
                }
            }
            return timeUnitComponent;
        }, {});
        if (isEmpty(formula)) {
            return null;
        }
        return new TimeUnitNode(parent, formula);
    }
    static makeFromTransform(parent, t) {
        const { timeUnit, ...other } = { ...t };
        const normalizedTimeUnit = normalizeTimeUnit(timeUnit);
        const component = {
            ...other,
            timeUnit: normalizedTimeUnit,
        };
        return new TimeUnitNode(parent, {
            [hash(component)]: component,
        });
    }
    /**
     * Merge together TimeUnitNodes assigning the children of `other` to `this`
     * and removing `other`.
     */
    merge(other) {
        this.timeUnits = { ...this.timeUnits };
        // if the same hash happen twice, merge
        for (const key in other.timeUnits) {
            if (!this.timeUnits[key]) {
                // copy if it's not a duplicate
                this.timeUnits[key] = other.timeUnits[key];
            }
        }
        for (const child of other.children) {
            other.removeChild(child);
            child.parent = this;
        }
        other.remove();
    }
    /**
     * Remove time units coming from the other node.
     */
    removeFormulas(fields) {
        const newFormula = {};
        for (const [key, timeUnitComponent] of entries$1(this.timeUnits)) {
            const fieldAs = isTimeUnitTransformComponent(timeUnitComponent)
                ? timeUnitComponent.as
                : `${timeUnitComponent.field}_end`;
            if (!fields.has(fieldAs)) {
                newFormula[key] = timeUnitComponent;
            }
        }
        this.timeUnits = newFormula;
    }
    producedFields() {
        return new Set(vals(this.timeUnits).map((f) => {
            return isTimeUnitTransformComponent(f) ? f.as : offsetAs(f.field);
        }));
    }
    dependentFields() {
        return new Set(vals(this.timeUnits).map((f) => f.field));
    }
    hash() {
        return `TimeUnit ${hash(this.timeUnits)}`;
    }
    assemble() {
        const transforms = [];
        for (const f of vals(this.timeUnits)) {
            const { rectBandPosition } = f;
            const normalizedTimeUnit = normalizeTimeUnit(f.timeUnit);
            if (isTimeUnitTransformComponent(f)) {
                const { field, as } = f;
                const { unit, utc, ...params } = normalizedTimeUnit;
                const startEnd = [as, `${as}_end`];
                transforms.push({
                    field: replacePathInField(field),
                    type: 'timeunit',
                    ...(unit ? { units: getTimeUnitParts(unit) } : {}),
                    ...(utc ? { timezone: 'utc' } : {}),
                    ...params,
                    as: startEnd,
                });
                transforms.push(...offsetedRectFormulas(startEnd, rectBandPosition, normalizedTimeUnit));
            }
            else if (f) {
                const { field: escapedField } = f;
                // since this is a expression, we want the unescaped field name
                const field = unescapeSingleQuoteAndPathDot(escapedField);
                const expr = offsetExpr({ timeUnit: normalizedTimeUnit, field });
                const endAs = offsetAs(field);
                transforms.push({
                    type: 'formula',
                    expr,
                    as: endAs,
                });
                transforms.push(...offsetedRectFormulas([field, endAs], rectBandPosition, normalizedTimeUnit));
            }
        }
        return transforms;
    }
}
const OFFSETTED_RECT_START_SUFFIX = 'offsetted_rect_start';
const OFFSETTED_RECT_END_SUFFIX = 'offsetted_rect_end';
function offsetExpr({ timeUnit, field, reverse }) {
    const { unit, utc } = timeUnit;
    const smallestUnit = getSmallestTimeUnitPart(unit);
    const { part, step } = getDateTimePartAndStep(smallestUnit, timeUnit.step);
    const offsetFn = utc ? 'utcOffset' : 'timeOffset';
    const expr = `${offsetFn}('${part}', ${accessWithDatumToUnescapedPath(field)}, ${reverse ? -step : step})`;
    return expr;
}
function offsetedRectFormulas([startField, endField], rectBandPosition, timeUnit) {
    if (rectBandPosition !== undefined && rectBandPosition !== 0.5) {
        const startExpr = accessWithDatumToUnescapedPath(startField);
        const endExpr = accessWithDatumToUnescapedPath(endField);
        return [
            {
                type: 'formula',
                expr: interpolateExpr([
                    offsetExpr({
                        timeUnit,
                        field: startField,
                        reverse: true,
                    }),
                    startExpr,
                ], rectBandPosition + 0.5),
                as: `${startField}_${OFFSETTED_RECT_START_SUFFIX}`,
            },
            {
                type: 'formula',
                expr: interpolateExpr([startExpr, endExpr], rectBandPosition + 0.5),
                as: `${startField}_${OFFSETTED_RECT_END_SUFFIX}`,
            },
        ];
    }
    return [];
}
function interpolateExpr([start, end], fraction) {
    return `${1 - fraction} * ${start} + ${fraction} * ${end}`;
}

const TUPLE_FIELDS = '_tuple_fields';
class SelectionProjectionComponent {
    hasChannel;
    hasField;
    hasSelectionId;
    timeUnit;
    items;
    constructor(...items) {
        this.items = items;
        this.hasChannel = {};
        this.hasField = {};
        this.hasSelectionId = false;
    }
}
const project = {
    defined: () => {
        return true; // This transform handles its own defaults, so always run parse.
    },
    parse: (model, selCmpt, selDef) => {
        const name = selCmpt.name;
        const proj = (selCmpt.project ??= new SelectionProjectionComponent());
        const parsed = {};
        const timeUnits = {};
        const signals = new Set();
        const signalName = (p, range) => {
            const suffix = range === 'visual' ? p.channel : p.field;
            let sg = varName(`${name}_${suffix}`);
            for (let counter = 1; signals.has(sg); counter++) {
                sg = varName(`${name}_${suffix}_${counter}`);
            }
            signals.add(sg);
            return { [range]: sg };
        };
        const type = selCmpt.type;
        const cfg = model.config.selection[type];
        const init = selDef.value !== undefined
            ? array$2(selDef.value)
            : null;
        // If no explicit projection (either fields or encodings) is specified, set some defaults.
        // If an initial value is set, try to infer projections.
        let { fields, encodings } = (isObject(selDef.select) ? selDef.select : {});
        if (!fields && !encodings && init) {
            for (const initVal of init) {
                // initVal may be a scalar value to smoothen varParam -> pointSelection gradient.
                if (!isObject(initVal)) {
                    continue;
                }
                for (const key of keys(initVal)) {
                    if (isSingleDefUnitChannel(key)) {
                        (encodings || (encodings = [])).push(key);
                    }
                    else {
                        if (type === 'interval') {
                            warn(INTERVAL_INITIALIZED_WITH_POS);
                            encodings = cfg.encodings;
                        }
                        else {
                            (fields ??= []).push(key);
                        }
                    }
                }
            }
        }
        // If no initial value is specified, use the default configuration.
        // We break this out as a separate if block (instead of an else condition)
        // to account for unprojected point selections that have scalar initial values
        if (!fields && !encodings) {
            encodings = cfg.encodings;
            if ('fields' in cfg) {
                fields = cfg.fields;
            }
        }
        for (const channel of encodings ?? []) {
            const fieldDef = model.fieldDef(channel);
            if (fieldDef) {
                let field = fieldDef.field;
                if (fieldDef.aggregate) {
                    warn(cannotProjectAggregate(channel, fieldDef.aggregate));
                    continue;
                }
                else if (!field) {
                    warn(cannotProjectOnChannelWithoutField(channel));
                    continue;
                }
                if (fieldDef.timeUnit && !isBinnedTimeUnit(fieldDef.timeUnit)) {
                    field = model.vgField(channel);
                    // Construct TimeUnitComponents which will be combined into a
                    // TimeUnitNode. This node may need to be inserted into the
                    // dataflow if the selection is used across views that do not
                    // have these time units defined.
                    const component = {
                        timeUnit: fieldDef.timeUnit,
                        as: field,
                        field: fieldDef.field,
                    };
                    timeUnits[hash(component)] = component;
                }
                // Prevent duplicate projections on the same field.
                // TODO: what if the same field is bound to multiple channels (e.g., SPLOM diag).
                if (!parsed[field]) {
                    // Determine whether the tuple will store enumerated or ranged values.
                    // Interval selections store ranges for continuous scales, and enumerations otherwise.
                    // Single/multi selections store ranges for binned fields, and enumerations otherwise.
                    const tplType = type === 'interval' &&
                        isScaleChannel(channel) &&
                        hasContinuousDomain(model.getScaleComponent(channel).get('type'))
                        ? 'R'
                        : fieldDef.bin
                            ? 'R-RE'
                            : 'E';
                    const p = { field, channel, type: tplType, index: proj.items.length };
                    p.signals = { ...signalName(p, 'data'), ...signalName(p, 'visual') };
                    proj.items.push((parsed[field] = p));
                    proj.hasField[field] = parsed[field];
                    proj.hasSelectionId = proj.hasSelectionId || field === SELECTION_ID;
                    if (isGeoPositionChannel(channel)) {
                        p.geoChannel = channel;
                        p.channel = getPositionChannelFromLatLong(channel);
                        proj.hasChannel[p.channel] = parsed[field];
                    }
                    else {
                        proj.hasChannel[channel] = parsed[field];
                    }
                }
            }
            else {
                warn(cannotProjectOnChannelWithoutField(channel));
            }
        }
        for (const field of fields ?? []) {
            if (proj.hasField[field])
                continue;
            const p = { type: 'E', field, index: proj.items.length };
            p.signals = { ...signalName(p, 'data') };
            proj.items.push(p);
            proj.hasField[field] = p;
            proj.hasSelectionId = proj.hasSelectionId || field === SELECTION_ID;
        }
        if (init) {
            selCmpt.init = init.map((v) => {
                // Selections can be initialized either with a full object that maps projections to values
                // or scalar values to smoothen the abstraction gradient from variable params to point selections.
                return proj.items.map((p) => isObject(v) ? (v[p.geoChannel || p.channel] !== undefined ? v[p.geoChannel || p.channel] : v[p.field]) : v);
            });
        }
        if (!isEmpty(timeUnits)) {
            proj.timeUnit = new TimeUnitNode(null, timeUnits);
        }
    },
    signals: (model, selCmpt, allSignals) => {
        const name = selCmpt.name + TUPLE_FIELDS;
        const hasSignal = allSignals.filter((s) => s.name === name);
        return hasSignal.length > 0 || selCmpt.project.hasSelectionId
            ? allSignals
            : allSignals.concat({
                name,
                value: selCmpt.project.items.map(assembleProjection),
            });
    },
};

const CURR = '_curr';
const ANIM_VALUE = 'anim_value';
const ANIM_CLOCK = 'anim_clock';
const EASED_ANIM_CLOCK = 'eased_anim_clock';
const MIN_EXTENT = 'min_extent';
const MAX_RANGE_EXTENT = 'max_range_extent';
const LAST_TICK = 'last_tick_at';
const IS_PLAYING = 'is_playing';
const THROTTLE = (1 / 60) * 1000; // 60 FPS
const animationSignals = (selectionName, scaleName) => {
    return [
        // timer signals
        {
            name: EASED_ANIM_CLOCK,
            // update: 'easeLinear(anim_clock / max_range_extent) * max_range_extent'
            update: ANIM_CLOCK, // TODO: replace with above once easing functions are implemented in vega-functions
        },
        // scale signals
        // TODO(jzong): uncomment commented signals below when implementing interpolation
        { name: `${selectionName}_domain`, init: `domain('${scaleName}')` },
        { name: MIN_EXTENT, init: `extent(${selectionName}_domain)[0]` },
        // {name: 'max_extent', init: `extent(${selectionName}_domain)[1]`},
        { name: MAX_RANGE_EXTENT, init: `extent(range('${scaleName}'))[1]` },
        // {name: 't_index', update: `indexof(${selectionName}_domain, anim_value)`},
        { name: ANIM_VALUE, update: `invert('${scaleName}', ${EASED_ANIM_CLOCK})` },
    ];
};
const point$1 = {
    defined: (selCmpt) => selCmpt.type === 'point',
    topLevelSignals: (model, selCmpt, signals) => {
        if (isTimerSelection(selCmpt)) {
            signals = signals.concat([
                {
                    name: ANIM_CLOCK,
                    init: '0',
                    on: [
                        {
                            events: { type: 'timer', throttle: THROTTLE },
                            update: `${IS_PLAYING} ? (${ANIM_CLOCK} + (now() - ${LAST_TICK}) > ${MAX_RANGE_EXTENT} ? 0 : ${ANIM_CLOCK} + (now() - ${LAST_TICK})) : ${ANIM_CLOCK}`,
                        },
                    ],
                },
                {
                    name: LAST_TICK,
                    init: 'now()',
                    on: [{ events: [{ signal: ANIM_CLOCK }, { signal: IS_PLAYING }], update: 'now()' }],
                },
                {
                    name: IS_PLAYING,
                    init: 'true',
                },
            ]);
        }
        return signals;
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const fieldsSg = name + TUPLE_FIELDS;
        const project = selCmpt.project;
        const datum = '(item().isVoronoi ? datum.datum : datum)';
        // Only add a discrete selection to the store if a datum is present _and_
        // the interaction isn't occurring on a group mark. This guards against
        // polluting interactive state with invalid values in faceted displays
        // as the group marks are also data-driven. We force the update to account
        // for constant null states but varying toggles (e.g., shift-click in
        // whitespace followed by a click in whitespace; the store should only
        // be cleared on the second click).
        const brushes = vals(model.component.selection ?? {})
            .reduce((acc, cmpt) => {
            return cmpt.type === 'interval' ? acc.concat(cmpt.name + BRUSH) : acc;
        }, [])
            .map((b) => `indexof(item().mark.name, '${b}') < 0`)
            .join(' && ');
        const test = `datum && item().mark.marktype !== 'group' && indexof(item().mark.role, 'legend') < 0${brushes ? ` && ${brushes}` : ''}`;
        let update = `unit: ${unitName(model)}, `;
        if (selCmpt.project.hasSelectionId) {
            update += `${SELECTION_ID}: ${datum}[${$(SELECTION_ID)}]`;
        }
        else if (isTimerSelection(selCmpt)) {
            update += `fields: ${fieldsSg}, values: [${ANIM_VALUE} ? ${ANIM_VALUE} : ${MIN_EXTENT}]`;
        }
        else {
            const values = project.items
                .map((p) => {
                const fieldDef = model.fieldDef(p.channel);
                // Binned fields should capture extents, for a range test against the raw field.
                return fieldDef?.bin
                    ? `[${datum}[${$(model.vgField(p.channel, {}))}], ` +
                        `${datum}[${$(model.vgField(p.channel, { binSuffix: 'end' }))}]]`
                    : `${datum}[${$(p.field)}]`;
            })
                .join(', ');
            update += `fields: ${fieldsSg}, values: [${values}]`;
        }
        if (isTimerSelection(selCmpt)) {
            // timer event: selection is for animation
            return signals.concat(animationSignals(selCmpt.name, model.scaleName(TIME)), [
                {
                    name: name + TUPLE,
                    on: [
                        {
                            events: [{ signal: EASED_ANIM_CLOCK }, { signal: ANIM_VALUE }],
                            update: `{${update}}`,
                            force: true,
                        },
                    ],
                },
            ]);
        }
        else {
            const events = selCmpt.events;
            return signals.concat([
                {
                    name: name + TUPLE,
                    on: events
                        ? [
                            {
                                events,
                                update: `${test} ? {${update}} : null`,
                                force: true,
                            },
                        ]
                        : [],
                },
            ]);
        }
    },
};

function assembleProjection(proj) {
    const { signals, hasLegend, index, ...rest } = proj;
    rest.field = replacePathInField(rest.field);
    return rest;
}
function assembleInit(init, isExpr = true, wrap = identity) {
    if (isArray(init)) {
        const assembled = init.map((v) => assembleInit(v, isExpr, wrap));
        return isExpr ? `[${assembled.join(', ')}]` : assembled;
    }
    else if (isDateTime(init)) {
        if (isExpr) {
            return wrap(dateTimeToExpr(init));
        }
        else {
            return wrap(dateTimeToTimestamp(init));
        }
    }
    return isExpr ? wrap(stringify$1(init)) : init;
}
function assembleUnitSelectionSignals(model, signals) {
    for (const selCmpt of vals(model.component.selection ?? {})) {
        const name = selCmpt.name;
        let modifyExpr = `${name}${TUPLE}, ${selCmpt.resolve === 'global' ? 'true' : `{unit: ${unitName(model)}}`}`;
        for (const c of selectionCompilers) {
            if (!c.defined(selCmpt))
                continue;
            if (c.signals)
                signals = c.signals(model, selCmpt, signals);
            if (c.modifyExpr)
                modifyExpr = c.modifyExpr(model, selCmpt, modifyExpr);
        }
        signals.push({
            name: name + MODIFY,
            on: [
                {
                    events: { signal: selCmpt.name + TUPLE },
                    update: `modify(${$(selCmpt.name + STORE)}, ${modifyExpr})`,
                },
            ],
        });
    }
    return cleanupEmptyOnArray(signals);
}
function assembleFacetSignals(model, signals) {
    if (model.component.selection && keys(model.component.selection).length) {
        const name = $(model.getName('cell'));
        signals.unshift({
            name: 'facet',
            value: {},
            on: [
                {
                    events: eventSelector('pointermove', 'scope'),
                    update: `isTuple(facet) ? facet : group(${name}).datum`,
                },
            ],
        });
    }
    return cleanupEmptyOnArray(signals);
}
function assembleTopLevelSignals(model, signals) {
    let hasSelections = false;
    for (const selCmpt of vals(model.component.selection ?? {})) {
        const name = selCmpt.name;
        const store = $(name + STORE);
        const hasSg = signals.filter((s) => s.name === name);
        if (hasSg.length === 0) {
            const resolve = selCmpt.resolve === 'global' ? 'union' : selCmpt.resolve;
            const isPoint = selCmpt.type === 'point' ? ', true, true)' : ')';
            signals.push({
                name: selCmpt.name,
                update: `${VL_SELECTION_RESOLVE}(${store}, ${$(resolve)}${isPoint}`,
            });
        }
        hasSelections = true;
        for (const c of selectionCompilers) {
            if (c.defined(selCmpt) && c.topLevelSignals) {
                signals = c.topLevelSignals(model, selCmpt, signals);
            }
        }
    }
    if (hasSelections) {
        const hasUnit = signals.filter((s) => s.name === 'unit');
        if (hasUnit.length === 0) {
            signals.unshift({
                name: 'unit',
                value: {},
                on: [{ events: 'pointermove', update: 'isTuple(group()) ? group() : unit' }],
            });
        }
    }
    return cleanupEmptyOnArray(signals);
}
function assembleUnitSelectionData(model, data) {
    const selectionData = [];
    const animationData = [];
    const unit = unitName(model, { escape: false });
    for (const selCmpt of vals(model.component.selection ?? {})) {
        const store = { name: selCmpt.name + STORE };
        if (selCmpt.project.hasSelectionId) {
            store.transform = [{ type: 'collect', sort: { field: SELECTION_ID } }];
        }
        if (selCmpt.init) {
            const fields = selCmpt.project.items.map(assembleProjection);
            store.values = selCmpt.project.hasSelectionId
                ? selCmpt.init.map((i) => ({ unit, [SELECTION_ID]: assembleInit(i, false)[0] }))
                : selCmpt.init.map((i) => ({ unit, fields, values: assembleInit(i, false) }));
        }
        const contains = [...selectionData, ...data].filter((d) => d.name === selCmpt.name + STORE);
        if (!contains.length) {
            selectionData.push(store);
        }
        if (isTimerSelection(selCmpt) && data.length) {
            // TODO(jzong): eventually uncomment this stuff when we want to support multi-view
            // const sourceName =
            //   model.parent && model.parent.type !== 'unit' // facet, layer, or concat
            //     ? model.parent.lookupDataSource(model.parent.getDataName(DataSourceType.Main))
            //     : model.lookupDataSource(model.getDataName(DataSourceType.Main));
            const sourceName = model.lookupDataSource(model.getDataName(DataSourceType.Main));
            const sourceData = data.find((d) => d.name === sourceName);
            // find the filter transform for the current selection
            const sourceDataFilter = sourceData.transform.find((t) => t.type === 'filter' && t.expr.includes('vlSelectionTest'));
            if (sourceDataFilter) {
                // remove it from the original dataset
                sourceData.transform = sourceData.transform.filter((t) => t !== sourceDataFilter);
                // create dataset to hold current animation frame
                const currentFrame = {
                    name: sourceData.name + CURR,
                    source: sourceData.name,
                    transform: [sourceDataFilter], // add the selection filter to the animation dataset
                };
                animationData.push(currentFrame);
            }
        }
    }
    return selectionData.concat(data, animationData);
}
function assembleUnitSelectionMarks(model, marks) {
    for (const selCmpt of vals(model.component.selection ?? {})) {
        for (const c of selectionCompilers) {
            if (c.defined(selCmpt) && c.marks) {
                marks = c.marks(model, selCmpt, marks);
            }
        }
    }
    return marks;
}
function assembleLayerSelectionMarks(model, marks) {
    for (const child of model.children) {
        if (isUnitModel(child)) {
            marks = assembleUnitSelectionMarks(child, marks);
        }
    }
    return marks;
}
function assembleSelectionScaleDomain(model, extent, scaleCmpt, domain) {
    const parsedExtent = parseSelectionExtent(model, extent.param, extent);
    return {
        signal: hasContinuousDomain(scaleCmpt.get('type')) && isArray(domain) && domain[0] > domain[1]
            ? `isValid(${parsedExtent}) && reverse(${parsedExtent})`
            : parsedExtent,
    };
}
function cleanupEmptyOnArray(signals) {
    return signals.map((s) => {
        if (s.on && !s.on.length)
            delete s.on;
        return s;
    });
}

const scaleBindings = {
    defined: (selCmpt) => {
        return selCmpt.type === 'interval' && selCmpt.resolve === 'global' && selCmpt.bind && selCmpt.bind === 'scales';
    },
    parse: (model, selCmpt) => {
        const bound = (selCmpt.scales = []);
        for (const proj of selCmpt.project.items) {
            const channel = proj.channel;
            if (!isScaleChannel(channel)) {
                continue;
            }
            const scale = model.getScaleComponent(channel);
            const scaleType = scale ? scale.get('type') : undefined;
            if (scaleType == 'sequential') {
                warn(SEQUENTIAL_SCALE_DEPRECATED);
            }
            if (!scale || !hasContinuousDomain(scaleType)) {
                warn(SCALE_BINDINGS_CONTINUOUS);
                continue;
            }
            scale.set('selectionExtent', { param: selCmpt.name, field: proj.field }, true);
            bound.push(proj);
        }
    },
    topLevelSignals: (model, selCmpt, signals) => {
        const bound = selCmpt.scales.filter((proj) => signals.filter((s) => s.name === proj.signals.data).length === 0);
        // Top-level signals are only needed for multiview displays and if this
        // view's top-level signals haven't already been generated.
        if (!model.parent || isTopLevelLayer(model) || bound.length === 0) {
            return signals;
        }
        // vlSelectionResolve does not account for the behavior of bound scales in
        // multiview displays. Each unit view adds a tuple to the store, but the
        // state of the selection is the unit selection most recently updated. This
        // state is captured by the top-level signals that we insert and "push
        // outer" to from within the units. We need to reassemble this state into
        // the top-level named signal, except no single selCmpt has a global view.
        const namedSg = signals.find((s) => s.name === selCmpt.name);
        let update = namedSg.update;
        if (update.includes(VL_SELECTION_RESOLVE)) {
            namedSg.update = `{${bound
                .map((proj) => `${$(replacePathInField(proj.field))}: ${proj.signals.data}`)
                .join(', ')}}`;
        }
        else {
            for (const proj of bound) {
                const mapping = `${$(replacePathInField(proj.field))}: ${proj.signals.data}`;
                if (!update.includes(mapping)) {
                    update = `${update.substring(0, update.length - 1)}, ${mapping}}`;
                }
            }
            namedSg.update = update;
        }
        return signals.concat(bound.map((proj) => ({ name: proj.signals.data })));
    },
    signals: (model, selCmpt, signals) => {
        // Nested signals need only push to top-level signals with multiview displays.
        if (model.parent && !isTopLevelLayer(model)) {
            for (const proj of selCmpt.scales) {
                const signal = signals.find((s) => s.name === proj.signals.data);
                signal.push = 'outer';
                delete signal.value;
                delete signal.update;
            }
        }
        return signals;
    },
};
function domain(model, channel) {
    const scale = $(model.scaleName(channel));
    return `domain(${scale})`;
}
function isTopLevelLayer(model) {
    return model.parent && isLayerModel(model.parent) && (!model.parent.parent || isTopLevelLayer(model.parent.parent));
}

const BRUSH = '_brush';
const SCALE_TRIGGER = '_scale_trigger';
const GEO_INIT_TICK = 'geo_interval_init_tick'; // Workaround for https://github.com/vega/vega/issues/3481
const INIT = '_init';
const CENTER = '_center';
const interval = {
    defined: (selCmpt) => selCmpt.type === 'interval',
    parse: (model, selCmpt, selDef) => {
        if (model.hasProjection) {
            const def = { ...(isObject(selDef.select) ? selDef.select : {}) };
            def.fields = [SELECTION_ID];
            if (!def.encodings) {
                // Remap default x/y projection
                def.encodings = selDef.value ? keys(selDef.value) : [LONGITUDE, LATITUDE];
            }
            selDef.select = { type: 'interval', ...def };
        }
        if (selCmpt.translate && !scaleBindings.defined(selCmpt)) {
            const filterExpr = `!event.item || event.item.mark.name !== ${$(selCmpt.name + BRUSH)}`;
            for (const evt of selCmpt.events) {
                if (!evt.between) {
                    warn(`${evt} is not an ordered event stream for interval selections.`);
                    continue;
                }
                const filters = array$2((evt.between[0].filter ??= []));
                if (!filters.includes(filterExpr)) {
                    filters.push(filterExpr);
                }
            }
        }
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const tupleSg = name + TUPLE;
        const channels = vals(selCmpt.project.hasChannel).filter((p) => p.channel === X || p.channel === Y);
        const init = selCmpt.init ? selCmpt.init[0] : null;
        signals.push(...channels.reduce((arr, proj) => arr.concat(channelSignals(model, selCmpt, proj, init?.[proj.index])), []));
        if (!model.hasProjection) {
            // Proxy scale reactions to ensure that an infinite loop doesn't occur
            // when an interval selection filter touches the scale.
            if (!scaleBindings.defined(selCmpt)) {
                const triggerSg = name + SCALE_TRIGGER;
                const scaleTriggers = channels.map((proj) => {
                    const channel = proj.channel;
                    const { data: dname, visual: vname } = proj.signals;
                    const scaleName = $(model.scaleName(channel));
                    const scaleType = model.getScaleComponent(channel).get('type');
                    const toNum = hasContinuousDomain(scaleType) ? '+' : '';
                    return (`(!isArray(${dname}) || ` +
                        `(${toNum}invert(${scaleName}, ${vname})[0] === ${toNum}${dname}[0] && ` +
                        `${toNum}invert(${scaleName}, ${vname})[1] === ${toNum}${dname}[1]))`);
                });
                if (scaleTriggers.length) {
                    signals.push({
                        name: triggerSg,
                        value: {},
                        on: [
                            {
                                events: channels.map((proj) => ({ scale: model.scaleName(proj.channel) })),
                                update: `${scaleTriggers.join(' && ')} ? ${triggerSg} : {}`,
                            },
                        ],
                    });
                }
            }
            // Only add an interval to the store if it has valid data extents. Data extents
            // are set to null if pixel extents are equal to account for intervals over
            // ordinal/nominal domains which, when inverted, will still produce a valid datum.
            const dataSignals = channels.map((proj) => proj.signals.data);
            const update = `unit: ${unitName(model)}, fields: ${name + TUPLE_FIELDS}, values`;
            return signals.concat({
                name: tupleSg,
                ...(init ? { init: `{${update}: ${assembleInit(init)}}` } : {}),
                ...(dataSignals.length
                    ? {
                        on: [
                            {
                                events: [{ signal: dataSignals.join(' || ') }], // Prevents double invocation, see https://github.com/vega/vega/issues/1672.
                                update: `${dataSignals.join(' && ')} ? {${update}: [${dataSignals}]} : null`,
                            },
                        ],
                    }
                    : {}),
            });
        }
        else {
            const projection = $(model.projectionName());
            const centerSg = model.projectionName() + CENTER;
            const { x, y } = selCmpt.project.hasChannel;
            const xvname = x?.signals.visual;
            const yvname = y?.signals.visual;
            const xinit = x ? init?.[x.index] : `${centerSg}[0]`;
            const yinit = y ? init?.[y.index] : `${centerSg}[1]`;
            const sizeSg = (layout) => model.getSizeSignalRef(layout).signal;
            const bbox = `[` +
                `[${xvname ? `${xvname}[0]` : '0'}, ${yvname ? `${yvname}[0]` : '0'}],` +
                `[${xvname ? `${xvname}[1]` : sizeSg('width')}, ` +
                `${yvname ? `${yvname}[1]` : sizeSg('height')}]` +
                `]`;
            if (init) {
                signals.unshift({
                    name: name + INIT,
                    init: `[scale(${projection}, [${x ? xinit[0] : xinit}, ${y ? yinit[0] : yinit}]), ` +
                        `scale(${projection}, [${x ? xinit[1] : xinit}, ${y ? yinit[1] : yinit}])]`,
                });
                if (!x || !y) {
                    // If initializing a uni-dimensional brush, use the center of the view to determine the other coord
                    const hasCenterSg = signals.find((s) => s.name === centerSg);
                    if (!hasCenterSg) {
                        signals.unshift({
                            name: centerSg,
                            update: `invert(${projection}, [${sizeSg('width')}/2, ${sizeSg('height')}/2])`,
                        });
                    }
                }
            }
            const intersect = `intersect(${bbox}, {markname: ${$(model.getName('marks'))}}, unit.mark)`;
            const base = `{unit: ${unitName(model)}}`;
            const update = `vlSelectionTuples(${intersect}, ${base})`;
            const visualSignals = channels.map((proj) => proj.signals.visual);
            return signals.concat({
                name: tupleSg,
                on: [
                    {
                        events: [
                            ...(visualSignals.length ? [{ signal: visualSignals.join(' || ') }] : []),
                            ...(init ? [{ signal: GEO_INIT_TICK }] : []),
                        ],
                        update,
                    },
                ],
            });
        }
    },
    topLevelSignals: (model, selCmpt, signals) => {
        if (isUnitModel(model) && model.hasProjection && selCmpt.init) {
            // Workaround for https://github.com/vega/vega/issues/3481
            // The scenegraph isn't populated on the first pulse. So we use a timer signal
            // to re-pulse the dataflow as soon as possible. We return an object to ensure
            // this only occurs once.
            const hasTick = signals.filter((s) => s.name === GEO_INIT_TICK);
            if (!hasTick.length) {
                signals.unshift({
                    name: GEO_INIT_TICK,
                    value: null,
                    on: [
                        {
                            events: 'timer{1}',
                            update: `${GEO_INIT_TICK} === null ? {} : ${GEO_INIT_TICK}`,
                        },
                    ],
                });
            }
        }
        return signals;
    },
    marks: (model, selCmpt, marks) => {
        const name = selCmpt.name;
        const { x, y } = selCmpt.project.hasChannel;
        const xvname = x?.signals.visual;
        const yvname = y?.signals.visual;
        const store = `data(${$(selCmpt.name + STORE)})`;
        // Do not add a brush if we're binding to scales
        // or we don't have a valid interval projection
        if (scaleBindings.defined(selCmpt) || (!x && !y)) {
            return marks;
        }
        const update = {
            x: x !== undefined ? { signal: `${xvname}[0]` } : { value: 0 },
            y: y !== undefined ? { signal: `${yvname}[0]` } : { value: 0 },
            x2: x !== undefined ? { signal: `${xvname}[1]` } : { field: { group: 'width' } },
            y2: y !== undefined ? { signal: `${yvname}[1]` } : { field: { group: 'height' } },
        };
        // If the selection is resolved to global, only a single interval is in
        // the store. Wrap brush mark's encodings with a production rule to test
        // this based on the `unit` property. Hide the brush mark if it corresponds
        // to a unit different from the one in the store.
        if (selCmpt.resolve === 'global') {
            for (const key of keys(update)) {
                update[key] = [
                    {
                        test: `${store}.length && ${store}[0].unit === ${unitName(model)}`,
                        ...update[key],
                    },
                    { value: 0 },
                ];
            }
        }
        // Two brush marks ensure that fill colors and other aesthetic choices do
        // not interefere with the core marks, but that the brushed region can still
        // be interacted with (e.g., dragging it around).
        const { fill, fillOpacity, cursor, ...stroke } = selCmpt.mark;
        const vgStroke = keys(stroke).reduce((def, k) => {
            def[k] = [
                {
                    test: [
                        x !== undefined && `${xvname}[0] !== ${xvname}[1]`,
                        y !== undefined && `${yvname}[0] !== ${yvname}[1]`,
                    ]
                        .filter((t) => t)
                        .join(' && '),
                    value: stroke[k],
                },
                { value: null },
            ];
            return def;
        }, {});
        // Set cursor to move unless the brush cannot be translated
        const vgCursor = cursor ?? (selCmpt.translate ? 'move' : null);
        return [
            {
                name: `${name + BRUSH}_bg`,
                type: 'rect',
                clip: true,
                encode: {
                    enter: {
                        fill: { value: fill },
                        fillOpacity: { value: fillOpacity },
                    },
                    update,
                },
            },
            ...marks,
            {
                name: name + BRUSH,
                type: 'rect',
                clip: true,
                encode: {
                    enter: {
                        ...(vgCursor ? { cursor: { value: vgCursor } } : {}),
                        fill: { value: 'transparent' },
                    },
                    update: { ...update, ...vgStroke },
                },
            },
        ];
    },
};
/**
 * Returns the visual and data signals for an interval selection.
 */
function channelSignals(model, selCmpt, proj, init) {
    const scaledInterval = !model.hasProjection;
    const channel = proj.channel;
    const vname = proj.signals.visual;
    const scaleName = $(scaledInterval ? model.scaleName(channel) : model.projectionName());
    const scaled = (str) => `scale(${scaleName}, ${str})`;
    const size = model.getSizeSignalRef(channel === X ? 'width' : 'height').signal;
    const coord = `${channel}(unit)`;
    const von = selCmpt.events.reduce((def, evt) => {
        return [
            ...def,
            { events: evt.between[0], update: `[${coord}, ${coord}]` }, // Brush Start
            { events: evt, update: `[${vname}[0], clamp(${coord}, 0, ${size})]` }, // Brush End
        ];
    }, []);
    if (scaledInterval) {
        const dname = proj.signals.data;
        const hasScales = scaleBindings.defined(selCmpt);
        const scale = model.getScaleComponent(channel);
        const scaleType = scale ? scale.get('type') : undefined;
        const vinit = init ? { init: assembleInit(init, true, scaled) } : { value: [] };
        // React to pan/zooms of continuous scales. Non-continuous scales
        // (band, point) cannot be pan/zoomed and any other changes
        // to their domains (e.g., filtering) should clear the brushes.
        von.push({
            events: { signal: selCmpt.name + SCALE_TRIGGER },
            update: hasContinuousDomain(scaleType) ? `[${scaled(`${dname}[0]`)}, ${scaled(`${dname}[1]`)}]` : `[0, 0]`,
        });
        return hasScales
            ? [{ name: dname, on: [] }]
            : [
                { name: vname, ...vinit, on: von },
                {
                    name: dname,
                    ...(init ? { init: assembleInit(init) } : {}), // Cannot be `value` as `init` may require datetime exprs.
                    on: [
                        {
                            events: { signal: vname },
                            update: `${vname}[0] === ${vname}[1] ? null : invert(${scaleName}, ${vname})`,
                        },
                    ],
                },
            ];
    }
    else {
        const initIdx = channel === X ? 0 : 1;
        const initSg = selCmpt.name + INIT;
        const vinit = init ? { init: `[${initSg}[0][${initIdx}], ${initSg}[1][${initIdx}]]` } : { value: [] };
        return [{ name: vname, ...vinit, on: von }];
    }
}

/**
 * Return a VgEncodeEntry that includes a Vega production rule for a scale channel's encoding or guide encoding, which includes:
 * (1) the conditional rules (if provided as part of channelDef)
 * (2) invalidValueRef for handling invalid values (if provided as a parameter of this method)
 * (3) main reference for the encoded data.
 */
function wrapCondition({ model, channelDef, vgChannel, invalidValueRef, mainRefFn, }) {
    const condition = isConditionalDef(channelDef) && channelDef.condition;
    let valueRefs = [];
    if (condition) {
        const conditions = array$2(condition);
        valueRefs = conditions.map((c) => {
            const conditionValueRef = mainRefFn(c);
            if (isConditionalParameter(c)) {
                const { param, empty } = c;
                const test = parseSelectionPredicate(model, { param, empty });
                return { test, ...conditionValueRef };
            }
            else {
                const test = expression(model, c.test); // FIXME: remove casting once TS is no longer dumb about it
                return { test, ...conditionValueRef };
            }
        });
    }
    if (invalidValueRef !== undefined) {
        valueRefs.push(invalidValueRef);
    }
    const mainValueRef = mainRefFn(channelDef);
    if (mainValueRef !== undefined) {
        valueRefs.push(mainValueRef);
    }
    if (valueRefs.length > 1 ||
        (valueRefs.length === 1 && Boolean(valueRefs[0].test)) // We must use array form valueRefs if test exists, otherwise Vega won't execute the test.
    ) {
        return { [vgChannel]: valueRefs };
    }
    else if (valueRefs.length === 1) {
        return { [vgChannel]: valueRefs[0] };
    }
    return {};
}

function text$1(model, channel = 'text') {
    const channelDef = model.encoding[channel];
    return wrapCondition({
        model,
        channelDef,
        vgChannel: channel,
        mainRefFn: (cDef) => textRef(cDef, model.config),
        invalidValueRef: undefined, // text encoding doesn't have continuous scales and thus can't have invalid values
    });
}
function textRef(channelDef, config, expr = 'datum') {
    // text
    if (channelDef) {
        if (isValueDef(channelDef)) {
            return signalOrValueRef(channelDef.value);
        }
        if (isFieldOrDatumDef(channelDef)) {
            const { format, formatType } = getFormatMixins(channelDef);
            return formatSignalRef({ fieldOrDatumDef: channelDef, format, formatType, expr, config });
        }
    }
    return undefined;
}

function tooltip(model, opt = {}) {
    const { encoding, markDef, config, stack } = model;
    const channelDef = encoding.tooltip;
    if (isArray(channelDef)) {
        return { tooltip: tooltipRefForEncoding({ tooltip: channelDef }, stack, config, opt) };
    }
    else {
        const datum = opt.reactiveGeom ? 'datum.datum' : 'datum';
        const mainRefFn = (cDef) => {
            // use valueRef based on channelDef first
            const tooltipRefFromChannelDef = addLineBreaksToTooltip(cDef, config, datum);
            if (tooltipRefFromChannelDef) {
                return tooltipRefFromChannelDef;
            }
            if (cDef === null) {
                // Allow using encoding.tooltip = null to disable tooltip
                return undefined;
            }
            let markTooltip = getMarkPropOrConfig('tooltip', markDef, config);
            if (markTooltip === true) {
                markTooltip = { content: 'encoding' };
            }
            if (isString(markTooltip)) {
                return { value: markTooltip };
            }
            else if (isObject(markTooltip)) {
                // `tooltip` is `{fields: 'encodings' | 'fields'}`
                if (isSignalRef(markTooltip)) {
                    return markTooltip;
                }
                else if (markTooltip.content === 'encoding') {
                    return tooltipRefForEncoding(encoding, stack, config, opt);
                }
                else {
                    return { signal: datum };
                }
            }
            return undefined;
        };
        return wrapCondition({
            model,
            channelDef,
            vgChannel: 'tooltip',
            mainRefFn,
            invalidValueRef: undefined, // tooltip encoding doesn't have continuous scales and thus can't have invalid values
        });
    }
}
function tooltipData(encoding, stack, config, { reactiveGeom } = {}) {
    const formatConfig = { ...config, ...config.tooltipFormat };
    const toSkip = new Set();
    const expr = reactiveGeom ? 'datum.datum' : 'datum';
    const tuples = [];
    function add(fDef, channel) {
        const mainChannel = getMainRangeChannel(channel);
        const fieldDef = isTypedFieldDef(fDef)
            ? fDef
            : {
                ...fDef,
                type: encoding[mainChannel].type, // for secondary field def, copy type from main channel
            };
        const title = fieldDef.title || defaultTitle(fieldDef, formatConfig);
        const key = array$2(title).join(', ').replaceAll(/"/g, '\\"');
        let value;
        if (isXorY(channel)) {
            const channel2 = channel === 'x' ? 'x2' : 'y2';
            const fieldDef2 = getFieldDef(encoding[channel2]);
            if (isBinned(fieldDef.bin) && fieldDef2) {
                const startField = vgField(fieldDef, { expr });
                const endField = vgField(fieldDef2, { expr });
                const { format, formatType } = getFormatMixins(fieldDef);
                value = binFormatExpression(startField, endField, format, formatType, formatConfig);
                toSkip.add(channel2);
            }
        }
        if ((isXorY(channel) || channel === THETA || channel === RADIUS) &&
            stack &&
            stack.fieldChannel === channel &&
            stack.offset === 'normalize') {
            const { format, formatType } = getFormatMixins(fieldDef);
            value = formatSignalRef({
                fieldOrDatumDef: fieldDef,
                format,
                formatType,
                expr,
                config: formatConfig,
                normalizeStack: true,
            }).signal;
        }
        value ??= addLineBreaksToTooltip(fieldDef, formatConfig, expr).signal;
        tuples.push({ channel, key, value });
    }
    forEach(encoding, (channelDef, channel) => {
        if (isFieldDef(channelDef)) {
            add(channelDef, channel);
        }
        else if (hasConditionalFieldDef(channelDef)) {
            add(channelDef.condition, channel);
        }
    });
    const out = {};
    for (const { channel, key, value } of tuples) {
        if (!toSkip.has(channel) && !out[key]) {
            out[key] = value;
        }
    }
    return out;
}
function tooltipRefForEncoding(encoding, stack, config, { reactiveGeom } = {}) {
    const data = tooltipData(encoding, stack, config, { reactiveGeom });
    const keyValues = entries$1(data).map(([key, value]) => `"${key}": ${value}`);
    return keyValues.length > 0 ? { signal: `{${keyValues.join(', ')}}` } : undefined;
}
/**
 * Transforms a tooltip value that is an array to a string with line breaks
 */
function addLineBreaksToTooltip(channelDef, config, expr = 'datum') {
    if (isFieldDef(channelDef) &&
        isDiscrete$1(channelDef.type) &&
        !getFormatMixins(channelDef).format &&
        !getFormatMixins(channelDef).formatType) {
        const fieldString = `${expr}["${channelDef.field}"]`;
        return {
            signal: `isValid(${fieldString}) ? isArray(${fieldString}) ? join(${fieldString}, '\\n') : ${fieldString} : ""+${fieldString}`,
        };
    }
    return textRef(channelDef, config, expr);
}

function aria(model) {
    const { markDef, config } = model;
    const enableAria = getMarkPropOrConfig('aria', markDef, config);
    // We can ignore other aria properties if ariaHidden is true.
    if (enableAria === false) {
        // getMarkGroups sets aria to false already so we don't have to set it in the encode block
        return {};
    }
    return {
        ...(enableAria ? { aria: enableAria } : {}),
        ...ariaRoleDescription(model),
        ...description(model),
    };
}
function ariaRoleDescription(model) {
    const { mark, markDef, config } = model;
    if (config.aria === false) {
        return {};
    }
    const ariaRoleDesc = getMarkPropOrConfig('ariaRoleDescription', markDef, config);
    if (ariaRoleDesc != null) {
        return { ariaRoleDescription: { value: ariaRoleDesc } };
    }
    return has$1(VG_MARK_INDEX, mark) ? {} : { ariaRoleDescription: { value: mark } };
}
function description(model) {
    const { encoding, markDef, config, stack } = model;
    const channelDef = encoding.description;
    if (channelDef) {
        return wrapCondition({
            model,
            channelDef,
            vgChannel: 'description',
            mainRefFn: (cDef) => textRef(cDef, model.config),
            invalidValueRef: undefined, // aria encoding doesn't have continuous scales and thus can't have invalid values
        });
    }
    // Use default from mark def or config if defined.
    // Functions in encode usually just return undefined but since we are defining a default below, we need to check the default here.
    const descriptionValue = getMarkPropOrConfig('description', markDef, config);
    if (descriptionValue != null) {
        return {
            description: signalOrValueRef(descriptionValue),
        };
    }
    if (config.aria === false) {
        return {};
    }
    const data = tooltipData(encoding, stack, config);
    if (isEmpty(data)) {
        return undefined;
    }
    return {
        description: {
            signal: entries$1(data)
                .filter(([key]) => !key.startsWith('_')) // remove internal/private signals from aria description
                .map(([key, value]) => [key, value.replaceAll('\\n', ' ')]) // replace newlines with spaces in aria description
                .map(([key, value], index) => `"${index > 0 ? '; ' : ''}${key}: " + (${value})`)
                .join(' + '),
        },
    };
}

/**
 * Return encode for non-positional channels with scales. (Text doesn't have scale.)
 */
function nonPosition(channel, model, opt = {}) {
    const { markDef, encoding, config } = model;
    const { vgChannel } = opt;
    let { defaultRef, defaultValue } = opt;
    const channelDef = encoding[channel];
    if (defaultRef === undefined) {
        // prettier-ignore
        defaultValue ??= getMarkPropOrConfig(channel, markDef, config, {
            vgChannel,
            // If there is no conditonal def, we ignore vgConfig so the output spec is concise.
            // However, if there is a conditional def, we must include vgConfig so the default is respected.
            ignoreVgConfig: !isConditionalDef(channelDef)
        });
        if (defaultValue !== undefined) {
            defaultRef = signalOrValueRef(defaultValue);
        }
    }
    const commonProps = {
        markDef,
        config,
        scaleName: model.scaleName(channel),
        scale: model.getScaleComponent(channel),
    };
    const invalidValueRef = getConditionalValueRefForIncludingInvalidValue({
        ...commonProps,
        scaleChannel: channel,
        channelDef,
    });
    const mainRefFn = (cDef) => {
        return midPoint({
            ...commonProps,
            channel,
            channelDef: cDef,
            stack: null, // No need to provide stack for non-position as it does not affect mid point
            defaultRef,
        });
    };
    return wrapCondition({
        model,
        channelDef,
        vgChannel: vgChannel ?? channel,
        invalidValueRef,
        mainRefFn,
    });
}

function color(model, opt = { filled: undefined }) {
    const { markDef, encoding, config } = model;
    const { type: markType } = markDef;
    // Allow filled to be overridden (for trail's "filled")
    const filled = opt.filled ?? getMarkPropOrConfig('filled', markDef, config);
    const transparentIfNeeded = contains(['bar', 'point', 'circle', 'square', 'geoshape'], markType)
        ? 'transparent'
        : undefined;
    const defaultFill = getMarkPropOrConfig(filled === true ? 'color' : undefined, markDef, config, { vgChannel: 'fill' }) ??
        // need to add this manually as getMarkConfig normally drops config.mark[channel] if vgChannel is specified
        config.mark[filled === true && 'color'] ??
        // If there is no fill, always fill symbols, bar, geoshape
        // with transparent fills https://github.com/vega/vega-lite/issues/1316
        transparentIfNeeded;
    const defaultStroke = getMarkPropOrConfig(filled === false ? 'color' : undefined, markDef, config, { vgChannel: 'stroke' }) ??
        // need to add this manually as getMarkConfig normally drops config.mark[channel] if vgChannel is specified
        config.mark[filled === false && 'color'];
    const colorVgChannel = filled ? 'fill' : 'stroke';
    const fillStrokeMarkDefAndConfig = {
        ...(defaultFill ? { fill: signalOrValueRef(defaultFill) } : {}),
        ...(defaultStroke ? { stroke: signalOrValueRef(defaultStroke) } : {}),
    };
    if (markDef.color && (filled ? markDef.fill : markDef.stroke)) {
        warn(droppingColor('property', { fill: 'fill' in markDef, stroke: 'stroke' in markDef }));
    }
    return {
        ...fillStrokeMarkDefAndConfig,
        ...nonPosition('color', model, {
            vgChannel: colorVgChannel,
            defaultValue: filled ? defaultFill : defaultStroke,
        }),
        ...nonPosition('fill', model, {
            // if there is encoding.fill, include default fill just in case we have conditional-only fill encoding
            defaultValue: encoding.fill ? defaultFill : undefined,
        }),
        ...nonPosition('stroke', model, {
            // if there is encoding.stroke, include default fill just in case we have conditional-only stroke encoding
            defaultValue: encoding.stroke ? defaultStroke : undefined,
        }),
    };
}

function zindex(model) {
    const { encoding, mark } = model;
    const order = encoding.order;
    if (!isPathMark(mark) && isValueDef(order)) {
        return wrapCondition({
            model,
            channelDef: order,
            vgChannel: 'zindex',
            mainRefFn: (cd) => signalOrValueRef(cd.value),
            invalidValueRef: undefined, // zindex encoding doesn't have continuous scales and thus can't have invalid values
        });
    }
    return {};
}

function positionOffset({ channel: baseChannel, markDef, encoding = {}, model, bandPosition, }) {
    const channel = `${baseChannel}Offset`; // Need to cast as the type can't be inferred automatically
    const defaultValue = markDef[channel];
    // FIXME: remove as any
    const channelDef = encoding[channel];
    if ((channel === 'xOffset' || channel === 'yOffset') && channelDef) {
        const ref = midPoint({
            channel: channel,
            channelDef,
            markDef,
            config: model?.config,
            scaleName: model.scaleName(channel),
            scale: model.getScaleComponent(channel),
            stack: null,
            defaultRef: signalOrValueRef(defaultValue),
            bandPosition,
        });
        return { offsetType: 'encoding', offset: ref };
    }
    const markDefOffsetValue = markDef[channel];
    if (markDefOffsetValue) {
        return { offsetType: 'visual', offset: markDefOffsetValue };
    }
    return {};
}

/**
 * Return encode for point (non-band) position channels.
 */
function pointPosition(channel, model, { defaultPos, vgChannel, }) {
    const { encoding, markDef, config, stack } = model;
    const channelDef = encoding[channel];
    const channel2Def = encoding[getSecondaryRangeChannel(channel)];
    const scaleName = model.scaleName(channel);
    const scale = model.getScaleComponent(channel);
    const { offset, offsetType } = positionOffset({
        channel,
        markDef,
        encoding,
        model,
        bandPosition: 0.5,
    });
    // Get default position or position from mark def
    const defaultRef = pointPositionDefaultRef({
        model,
        defaultPos,
        channel,
        scaleName,
        scale,
    });
    const valueRef = !channelDef && isXorY(channel) && (encoding.latitude || encoding.longitude)
        ? // use geopoint output if there are lat/long and there is no point position overriding lat/long.
            { field: model.getName(channel) }
        : positionRef({
            channel,
            channelDef,
            channel2Def,
            markDef,
            config,
            scaleName,
            scale,
            stack,
            offset,
            defaultRef,
            bandPosition: offsetType === 'encoding' ? 0 : undefined,
        });
    return valueRef ? { [vgChannel || channel]: valueRef } : undefined;
}
// TODO: we need to find a way to refactor these so that scaleName is a part of scale
// but that's complicated. For now, this is a huge step moving forward.
/**
 * @return Vega ValueRef for normal x- or y-position without projection
 */
function positionRef(params) {
    const { channel, channelDef, scaleName, stack, offset, markDef } = params;
    // This isn't a part of midPoint because we use midPoint for non-position too
    if (isFieldOrDatumDef(channelDef) && stack && channel === stack.fieldChannel) {
        if (isFieldDef(channelDef)) {
            let bandPosition = channelDef.bandPosition;
            if (bandPosition === undefined && markDef.type === 'text' && (channel === 'radius' || channel === 'theta')) {
                // theta and radius of text mark should use bandPosition = 0.5 by default
                // so that labels for arc marks are centered automatically
                bandPosition = 0.5;
            }
            if (bandPosition !== undefined) {
                return interpolatedSignalRef({
                    scaleName,
                    fieldOrDatumDef: channelDef, // positionRef always have type
                    startSuffix: 'start',
                    bandPosition,
                    offset,
                });
            }
        }
        // x or y use stack_end so that stacked line's point mark use stack_end too.
        return valueRefForFieldOrDatumDef(channelDef, scaleName, { suffix: 'end' }, { offset });
    }
    return midPointRefWithPositionInvalidTest(params);
}
function pointPositionDefaultRef({ model, defaultPos, channel, scaleName, scale, }) {
    const { markDef, config } = model;
    return () => {
        const mainChannel = getMainRangeChannel(channel);
        const vgChannel = getVgPositionChannel(channel);
        const definedValueOrConfig = getMarkPropOrConfig(channel, markDef, config, { vgChannel });
        if (definedValueOrConfig !== undefined) {
            return widthHeightValueOrSignalRef(channel, definedValueOrConfig);
        }
        switch (defaultPos) {
            case 'zeroOrMin':
                return zeroOrMinOrMaxPosition({ scaleName, scale, mode: 'zeroOrMin', mainChannel, config });
            case 'zeroOrMax':
                return zeroOrMinOrMaxPosition({
                    scaleName,
                    scale,
                    mode: { zeroOrMax: { widthSignal: model.width.signal, heightSignal: model.height.signal } },
                    mainChannel,
                    config,
                });
            case 'mid': {
                const sizeRef = model[getSizeChannel(channel)];
                return { ...sizeRef, mult: 0.5 };
            }
        }
        // defaultPos === null
        return undefined;
    };
}
function zeroOrMinOrMaxPosition({ mainChannel, config, ...otherProps }) {
    const scaledValueRef = scaledZeroOrMinOrMax(otherProps);
    const { mode } = otherProps;
    if (scaledValueRef) {
        return scaledValueRef;
    }
    switch (mainChannel) {
        case 'radius': {
            if (mode === 'zeroOrMin') {
                return { value: 0 }; // min value
            }
            const { widthSignal, heightSignal } = mode.zeroOrMax;
            // max of radius is min(width, height) / 2
            return {
                signal: `min(${widthSignal},${heightSignal})/2`,
            };
        }
        case 'theta':
            return mode === 'zeroOrMin' ? { value: 0 } : { signal: '2*PI' };
        case 'x':
            return mode === 'zeroOrMin' ? { value: 0 } : { field: { group: 'width' } };
        case 'y':
            return mode === 'zeroOrMin' ? { field: { group: 'height' } } : { value: 0 };
    }
}

const ALIGNED_X_CHANNEL = {
    left: 'x',
    center: 'xc',
    right: 'x2',
};
const BASELINED_Y_CHANNEL = {
    top: 'y',
    middle: 'yc',
    bottom: 'y2',
};
function vgAlignedPositionChannel(channel, markDef, config, defaultAlign = 'middle') {
    if (channel === 'radius' || channel === 'theta') {
        return getVgPositionChannel(channel);
    }
    const alignChannel = channel === 'x' ? 'align' : 'baseline';
    const align = getMarkPropOrConfig(alignChannel, markDef, config);
    let alignExcludingSignal;
    if (isSignalRef(align)) {
        warn(rangeMarkAlignmentCannotBeExpression(alignChannel));
        alignExcludingSignal = undefined;
    }
    else {
        alignExcludingSignal = align;
    }
    // FIXME: remove as any
    if (channel === 'x') {
        return ALIGNED_X_CHANNEL[alignExcludingSignal || (defaultAlign === 'top' ? 'left' : 'center')];
    }
    else {
        return BASELINED_Y_CHANNEL[alignExcludingSignal || defaultAlign];
    }
}

/**
 * Utility for area/rule position, which can be either point or range.
 * (One of the axes should be point and the other should be range.)
 */
function pointOrRangePosition(channel, model, { defaultPos, defaultPos2, range, }) {
    if (range) {
        return rangePosition(channel, model, { defaultPos, defaultPos2 });
    }
    return pointPosition(channel, model, { defaultPos });
}
function rangePosition(channel, model, { defaultPos, defaultPos2, }) {
    const { markDef, config } = model;
    const channel2 = getSecondaryRangeChannel(channel);
    const sizeChannel = getSizeChannel(channel);
    const pos2Mixins = pointPosition2OrSize(model, defaultPos2, channel2);
    const vgChannel = pos2Mixins[sizeChannel]
        ? // If there is width/height, we need to position the marks based on the alignment.
            vgAlignedPositionChannel(channel, markDef, config)
        : // Otherwise, make sure to apply to the right Vg Channel (for arc mark)
            getVgPositionChannel(channel);
    return {
        ...pointPosition(channel, model, { defaultPos, vgChannel }),
        ...pos2Mixins,
    };
}
/**
 * Return encode for x2, y2.
 * If channel is not specified, return one channel based on orientation.
 */
function pointPosition2OrSize(model, defaultPos, channel) {
    const { encoding, mark, markDef, stack, config } = model;
    const baseChannel = getMainRangeChannel(channel);
    const sizeChannel = getSizeChannel(channel);
    const vgChannel = getVgPositionChannel(channel);
    const channelDef = encoding[baseChannel];
    const scaleName = model.scaleName(baseChannel);
    const scale = model.getScaleComponent(baseChannel);
    const { offset } = channel in encoding || channel in markDef
        ? positionOffset({ channel, markDef, encoding, model })
        : positionOffset({ channel: baseChannel, markDef, encoding, model });
    if (!channelDef && (channel === 'x2' || channel === 'y2') && (encoding.latitude || encoding.longitude)) {
        const vgSizeChannel = getSizeChannel(channel);
        const size = model.markDef[vgSizeChannel];
        if (size != null) {
            return {
                [vgSizeChannel]: { value: size },
            };
        }
        else {
            return {
                [vgChannel]: { field: model.getName(channel) },
            };
        }
    }
    const valueRef = position2Ref({
        channel,
        channelDef,
        channel2Def: encoding[channel],
        markDef,
        config,
        scaleName,
        scale,
        stack,
        offset,
        defaultRef: undefined,
    });
    if (valueRef !== undefined) {
        return { [vgChannel]: valueRef };
    }
    // TODO: check width/height encoding here once we add them
    // no x2/y2 encoding, then try to read x2/y2 or width/height based on precedence:
    // markDef > config.style > mark-specific config (config[mark]) > general mark config (config.mark)
    return (position2orSize(channel, markDef) ||
        position2orSize(channel, {
            [channel]: getMarkStyleConfig(channel, markDef, config.style),
            [sizeChannel]: getMarkStyleConfig(sizeChannel, markDef, config.style),
        }) ||
        position2orSize(channel, config[mark]) ||
        position2orSize(channel, config.mark) || {
        [vgChannel]: pointPositionDefaultRef({
            model,
            defaultPos,
            channel,
            scaleName,
            scale,
        })(),
    });
}
function position2Ref({ channel, channelDef, channel2Def, markDef, config, scaleName, scale, stack, offset, defaultRef, }) {
    if (isFieldOrDatumDef(channelDef) &&
        stack &&
        // If fieldChannel is X and channel is X2 (or Y and Y2)
        channel.charAt(0) === stack.fieldChannel.charAt(0)) {
        return valueRefForFieldOrDatumDef(channelDef, scaleName, { suffix: 'start' }, { offset });
    }
    return midPointRefWithPositionInvalidTest({
        channel,
        channelDef: channel2Def,
        scaleName,
        scale,
        stack,
        markDef,
        config,
        offset,
        defaultRef,
    });
}
function position2orSize(channel, markDef) {
    const sizeChannel = getSizeChannel(channel);
    const vgChannel = getVgPositionChannel(channel);
    if (markDef[vgChannel] !== undefined) {
        return { [vgChannel]: widthHeightValueOrSignalRef(channel, markDef[vgChannel]) };
    }
    else if (markDef[channel] !== undefined) {
        return { [vgChannel]: widthHeightValueOrSignalRef(channel, markDef[channel]) };
    }
    else if (markDef[sizeChannel]) {
        const dimensionSize = markDef[sizeChannel];
        if (isRelativeBandSize(dimensionSize)) {
            warn(relativeBandSizeNotSupported(sizeChannel));
        }
        else {
            return { [sizeChannel]: widthHeightValueOrSignalRef(channel, dimensionSize) };
        }
    }
    return undefined;
}

function rectPosition(model, channel) {
    const { config, encoding, markDef } = model;
    const mark = markDef.type;
    const channel2 = getSecondaryRangeChannel(channel);
    const sizeChannel = getSizeChannel(channel);
    const channelDef = encoding[channel];
    const channelDef2 = encoding[channel2];
    const scale = model.getScaleComponent(channel);
    const scaleType = scale ? scale.get('type') : undefined;
    const orient = markDef.orient;
    const hasSizeDef = encoding[sizeChannel] ??
        encoding.size ??
        getMarkPropOrConfig('size', markDef, config, { vgChannel: sizeChannel });
    const offsetScaleChannel = getOffsetChannel(channel);
    const isBarOrTickBand = (mark === 'bar' && (channel === 'x' ? orient === 'vertical' : orient === 'horizontal')) ||
        (mark === 'tick' && (channel === 'y' ? orient === 'vertical' : orient === 'horizontal'));
    // x, x2, and width -- we must specify two of these in all conditions
    if (isFieldDef(channelDef) &&
        (isBinning(channelDef.bin) || isBinned(channelDef.bin) || (channelDef.timeUnit && !channelDef2)) &&
        !(hasSizeDef && !isRelativeBandSize(hasSizeDef)) &&
        !encoding[offsetScaleChannel] &&
        !hasDiscreteDomain(scaleType)) {
        return rectBinPosition({
            fieldDef: channelDef,
            fieldDef2: channelDef2,
            channel,
            model,
        });
    }
    else if (((isFieldOrDatumDef(channelDef) && hasDiscreteDomain(scaleType)) || isBarOrTickBand) && !channelDef2) {
        return positionAndSize(channelDef, channel, model);
    }
    else {
        return rangePosition(channel, model, { defaultPos: 'zeroOrMax', defaultPos2: 'zeroOrMin' });
    }
}
function defaultSizeRef(sizeChannel, scaleName, scale, config, bandSize, hasFieldDef, mark) {
    if (isRelativeBandSize(bandSize)) {
        if (scale) {
            const scaleType = scale.get('type');
            if (scaleType === 'band') {
                let bandWidth = `bandwidth('${scaleName}')`;
                if (bandSize.band !== 1) {
                    bandWidth = `${bandSize.band} * ${bandWidth}`;
                }
                const minBandSize = getMarkConfig('minBandSize', { type: mark }, config);
                return { signal: minBandSize ? `max(${signalOrStringValue(minBandSize)}, ${bandWidth})` : bandWidth };
            }
            else if (bandSize.band !== 1) {
                warn(cannotUseRelativeBandSizeWithNonBandScale(scaleType));
                bandSize = undefined;
            }
        }
        else {
            return {
                mult: bandSize.band,
                field: { group: sizeChannel },
            };
        }
    }
    else if (isSignalRef(bandSize)) {
        return bandSize;
    }
    else if (bandSize) {
        return { value: bandSize };
    }
    // no valid band size
    if (scale) {
        const scaleRange = scale.get('range');
        if (isVgRangeStep(scaleRange) && isNumber$1(scaleRange.step)) {
            return { value: scaleRange.step - 2 };
        }
    }
    if (!hasFieldDef) {
        const { bandPaddingInner, barBandPaddingInner, rectBandPaddingInner, tickBandPaddingInner } = config.scale;
        const padding = getFirstDefined(bandPaddingInner, mark === 'tick' ? tickBandPaddingInner : mark === 'bar' ? barBandPaddingInner : rectBandPaddingInner); // this part is like paddingInner in scale.ts
        if (isSignalRef(padding)) {
            return { signal: `(1 - (${padding.signal})) * ${sizeChannel}` };
        }
        else if (isNumber$1(padding)) {
            return { signal: `${1 - padding} * ${sizeChannel}` };
        }
    }
    const defaultStep = getViewConfigDiscreteStep(config.view, sizeChannel);
    return { value: defaultStep - 2 };
}
/**
 * Output position encoding and its size encoding for continuous, point, and band scales.
 */
function positionAndSize(fieldDef, channel, model) {
    const { markDef, encoding, config, stack } = model;
    const orient = markDef.orient;
    const scaleName = model.scaleName(channel);
    const scale = model.getScaleComponent(channel);
    const vgSizeChannel = getSizeChannel(channel);
    const channel2 = getSecondaryRangeChannel(channel);
    const offsetScaleChannel = getOffsetChannel(channel);
    const offsetScaleName = model.scaleName(offsetScaleChannel);
    const offsetScale = model.getScaleComponent(getOffsetScaleChannel(channel));
    const useVlSizeChannel = 
    // Always uses size channel for ticks, because tick only calls rectPosition() for the size channel
    markDef.type === 'tick' ||
        // use "size" channel for bars, if there is orient and the channel matches the right orientation
        (orient === 'horizontal' && channel === 'y') ||
        (orient === 'vertical' && channel === 'x');
    // Use size encoding / mark property / config if it exists
    let sizeMixins;
    if (encoding.size || markDef.size) {
        if (useVlSizeChannel) {
            sizeMixins = nonPosition('size', model, {
                vgChannel: vgSizeChannel,
                defaultRef: signalOrValueRef(markDef.size),
            });
        }
        else {
            warn(cannotApplySizeToNonOrientedMark(markDef.type));
        }
    }
    const hasSizeFromMarkOrEncoding = !!sizeMixins;
    // Otherwise, apply default value
    const bandSize = getBandSize({
        channel,
        fieldDef,
        markDef,
        config,
        scaleType: (scale || offsetScale)?.get('type'),
        useVlSizeChannel,
    });
    sizeMixins = sizeMixins || {
        [vgSizeChannel]: defaultSizeRef(vgSizeChannel, offsetScaleName || scaleName, offsetScale || scale, config, bandSize, !!fieldDef, markDef.type),
    };
    /*
      Band scales with size value and all point scales, use xc/yc + band=0.5
  
      Otherwise (band scales that has size based on a band ref), use x/y with position band = (1 - size_band) / 2.
      In this case, size_band is the band specified in the x/y-encoding.
      By default band is 1, so `(1 - band) / 2` = 0.
      If band is 0.6, the the x/y position in such case should be `(1 - band) / 2` = 0.2
     */
    const defaultBandAlign = (scale || offsetScale)?.get('type') === 'band' && isRelativeBandSize(bandSize) && !hasSizeFromMarkOrEncoding
        ? 'top'
        : 'middle';
    const vgChannel = vgAlignedPositionChannel(channel, markDef, config, defaultBandAlign);
    const center = vgChannel === 'xc' || vgChannel === 'yc';
    const { offset, offsetType } = positionOffset({ channel, markDef, encoding, model, bandPosition: center ? 0.5 : 0 });
    const posRef = midPointRefWithPositionInvalidTest({
        channel,
        channelDef: fieldDef,
        markDef,
        config,
        scaleName,
        scale,
        stack,
        offset,
        defaultRef: pointPositionDefaultRef({ model, defaultPos: 'mid', channel, scaleName, scale }),
        bandPosition: center
            ? offsetType === 'encoding'
                ? 0
                : 0.5
            : isSignalRef(bandSize)
                ? { signal: `(1-${bandSize})/2` }
                : isRelativeBandSize(bandSize)
                    ? (1 - bandSize.band) / 2
                    : 0,
    });
    if (vgSizeChannel) {
        return { [vgChannel]: posRef, ...sizeMixins };
    }
    else {
        // otherwise, we must simulate size by setting position2 = position + size
        // (for theta/radius since Vega doesn't have thetaWidth/radiusWidth)
        const vgChannel2 = getVgPositionChannel(channel2);
        const sizeRef = sizeMixins[vgSizeChannel];
        const sizeOffset = offset ? { ...sizeRef, offset } : sizeRef;
        return {
            [vgChannel]: posRef,
            // posRef might be an array that wraps position invalid test
            [vgChannel2]: isArray(posRef)
                ? [posRef[0], { ...posRef[1], offset: sizeOffset }]
                : {
                    ...posRef,
                    offset: sizeOffset,
                },
        };
    }
}
function getBinSpacing(channel, spacing, reverse, axisTranslate, offset, minBandSize, bandSizeExpr) {
    if (isPolarPositionChannel(channel)) {
        return 0;
    }
    const isEnd = channel === 'x' || channel === 'y2';
    const spacingOffset = isEnd ? -spacing / 2 : spacing / 2;
    if (isSignalRef(reverse) || isSignalRef(offset) || isSignalRef(axisTranslate) || minBandSize) {
        const reverseExpr = signalOrStringValue(reverse);
        const offsetExpr = signalOrStringValue(offset);
        const axisTranslateExpr = signalOrStringValue(axisTranslate);
        const minBandSizeExpr = signalOrStringValue(minBandSize);
        const sign = isEnd ? '' : '-';
        const spacingAndSizeOffset = minBandSize
            ? `(${bandSizeExpr} < ${minBandSizeExpr} ? ${sign}0.5 * (${minBandSizeExpr} - (${bandSizeExpr})) : ${spacingOffset})`
            : spacingOffset;
        const t = axisTranslateExpr ? `${axisTranslateExpr} + ` : '';
        const r = reverseExpr ? `(${reverseExpr} ? -1 : 1) * ` : '';
        const o = offsetExpr ? `(${offsetExpr} + ${spacingAndSizeOffset})` : spacingAndSizeOffset;
        return {
            signal: t + r + o,
        };
    }
    else {
        offset = offset || 0;
        // FIXME: remove this override
        // eslint-disable-next-line @typescript-eslint/no-unsafe-unary-minus
        return axisTranslate + (reverse ? -offset - spacingOffset : +offset + spacingOffset);
    }
}
function rectBinPosition({ fieldDef, fieldDef2, channel, model, }) {
    const { config, markDef, encoding } = model;
    const scale = model.getScaleComponent(channel);
    const scaleName = model.scaleName(channel);
    const scaleType = scale ? scale.get('type') : undefined;
    const reverse = scale.get('reverse');
    const bandSize = getBandSize({ channel, fieldDef, markDef, config, scaleType });
    const axis = model.component.axes[channel]?.[0];
    const axisTranslate = axis?.get('translate') ?? 0.5; // vega default is 0.5
    const spacing = isXorY(channel) ? (getMarkPropOrConfig('binSpacing', markDef, config) ?? 0) : 0;
    const channel2 = getSecondaryRangeChannel(channel);
    const vgChannel = getVgPositionChannel(channel);
    const vgChannel2 = getVgPositionChannel(channel2);
    const minBandSize = getMarkConfig('minBandSize', markDef, config);
    const { offset } = positionOffset({ channel, markDef, encoding, model, bandPosition: 0 });
    const { offset: offset2 } = positionOffset({ channel: channel2, markDef, encoding, model, bandPosition: 0 });
    const bandSizeExpr = binSizeExpr({ fieldDef, scaleName });
    const binSpacingOffset = getBinSpacing(channel, spacing, reverse, axisTranslate, offset, minBandSize, bandSizeExpr);
    const binSpacingOffset2 = getBinSpacing(channel2, spacing, reverse, axisTranslate, offset2 ?? offset, minBandSize, bandSizeExpr);
    const bandPositionForBandSize = isSignalRef(bandSize)
        ? { signal: `(1-${bandSize.signal})/2` }
        : isRelativeBandSize(bandSize)
            ? (1 - bandSize.band) / 2
            : 0.5;
    const bandPosition = getBandPosition({ fieldDef, fieldDef2, markDef, config });
    if (isBinning(fieldDef.bin) || fieldDef.timeUnit) {
        const useRectOffsetField = fieldDef.timeUnit && bandPosition !== 0.5;
        return {
            [vgChannel2]: rectBinRef({
                fieldDef,
                scaleName,
                bandPosition: bandPositionForBandSize,
                offset: binSpacingOffset2,
                useRectOffsetField,
            }),
            [vgChannel]: rectBinRef({
                fieldDef,
                scaleName,
                bandPosition: isSignalRef(bandPositionForBandSize)
                    ? { signal: `1-${bandPositionForBandSize.signal}` }
                    : 1 - bandPositionForBandSize,
                offset: binSpacingOffset,
                useRectOffsetField,
            }),
        };
    }
    else if (isBinned(fieldDef.bin)) {
        const startRef = valueRefForFieldOrDatumDef(fieldDef, scaleName, {}, { offset: binSpacingOffset2 });
        if (isFieldDef(fieldDef2)) {
            return {
                [vgChannel2]: startRef,
                [vgChannel]: valueRefForFieldOrDatumDef(fieldDef2, scaleName, {}, { offset: binSpacingOffset }),
            };
        }
        else if (isBinParams(fieldDef.bin) && fieldDef.bin.step) {
            return {
                [vgChannel2]: startRef,
                [vgChannel]: {
                    signal: `scale("${scaleName}", ${vgField(fieldDef, { expr: 'datum' })} + ${fieldDef.bin.step})`,
                    offset: binSpacingOffset,
                },
            };
        }
    }
    warn(channelRequiredForBinned(channel2));
    return undefined;
}
/**
 * Value Ref for binned fields
 */
function rectBinRef({ fieldDef, scaleName, bandPosition, offset, useRectOffsetField, }) {
    return interpolatedSignalRef({
        scaleName,
        fieldOrDatumDef: fieldDef,
        bandPosition,
        offset,
        ...(useRectOffsetField
            ? {
                startSuffix: OFFSETTED_RECT_START_SUFFIX,
                endSuffix: OFFSETTED_RECT_END_SUFFIX,
            }
            : {}),
    });
}

const ALWAYS_IGNORE = new Set(['aria', 'width', 'height']);
function baseEncodeEntry(model, ignore) {
    const { fill = undefined, stroke = undefined } = ignore.color === 'include' ? color(model) : {};
    return {
        ...markDefProperties(model.markDef, ignore),
        ...colorRef('fill', fill),
        ...colorRef('stroke', stroke),
        ...nonPosition('opacity', model),
        ...nonPosition('fillOpacity', model),
        ...nonPosition('strokeOpacity', model),
        ...nonPosition('strokeWidth', model),
        ...nonPosition('strokeDash', model),
        ...zindex(model),
        ...tooltip(model),
        ...text$1(model, 'href'),
        ...aria(model),
    };
}
function colorRef(channel, valueRef) {
    return valueRef ? { [channel]: valueRef } : {};
}
function markDefProperties(mark, ignore) {
    return VG_MARK_CONFIGS.reduce((m, prop) => {
        if (!ALWAYS_IGNORE.has(prop) && hasProperty(mark, prop) && ignore[prop] !== 'ignore') {
            m[prop] = signalOrValueRef(mark[prop]);
        }
        return m;
    }, {});
}

/**
 * Create Vega's "defined" encoding to break paths in a path mark for invalid values.
 */
function defined(model) {
    const { config, markDef } = model;
    // For each channel (x/y), add fields to break path to a set first.
    const fieldsToBreakPath = new Set();
    model.forEachFieldDef((fieldDef, channel) => {
        let scaleType;
        if (!isScaleChannel(channel) || !(scaleType = model.getScaleType(channel))) {
            // Skip if the channel is not a scale channel or does not have a scale
            return;
        }
        const isCountAggregate = isCountingAggregateOp(fieldDef.aggregate);
        const invalidDataMode = getScaleInvalidDataMode({
            scaleChannel: channel,
            markDef,
            config,
            scaleType,
            isCountAggregate,
        });
        if (shouldBreakPath(invalidDataMode)) {
            const field = model.vgField(channel, { expr: 'datum', binSuffix: model.stack?.impute ? 'mid' : undefined });
            if (field) {
                fieldsToBreakPath.add(field);
            }
        }
    });
    // If the set is not empty, return a defined signal.
    if (fieldsToBreakPath.size > 0) {
        const signal = [...fieldsToBreakPath].map((field) => fieldValidPredicate(field, true)).join(' && ');
        return { defined: { signal } };
    }
    return undefined;
}
function valueIfDefined(prop, value) {
    if (value !== undefined) {
        return { [prop]: signalOrValueRef(value) };
    }
    return undefined;
}

const VORONOI = 'voronoi';
const nearest = {
    defined: (selCmpt) => {
        return selCmpt.type === 'point' && selCmpt.nearest;
    },
    parse: (model, selCmpt) => {
        // Scope selection events to the voronoi mark to prevent capturing
        // events that occur on the group mark (https://github.com/vega/vega/issues/2112).
        if (selCmpt.events) {
            for (const s of selCmpt.events) {
                s.markname = model.getName(VORONOI);
            }
        }
    },
    marks: (model, selCmpt, marks) => {
        const { x, y } = selCmpt.project.hasChannel;
        const markType = model.mark;
        if (isPathMark(markType)) {
            warn(nearestNotSupportForContinuous(markType));
            return marks;
        }
        const cellDef = {
            name: model.getName(VORONOI),
            type: 'path',
            interactive: true,
            aria: false,
            from: { data: model.getName('marks') },
            encode: {
                update: {
                    fill: { value: 'transparent' },
                    strokeWidth: { value: 0.35 },
                    stroke: { value: 'transparent' },
                    isVoronoi: { value: true },
                    ...tooltip(model, { reactiveGeom: true }),
                },
            },
            transform: [
                {
                    type: 'voronoi',
                    x: { expr: x || !y ? 'datum.datum.x || 0' : '0' },
                    y: { expr: y || !x ? 'datum.datum.y || 0' : '0' },
                    size: [model.getSizeSignalRef('width'), model.getSizeSignalRef('height')],
                },
            ],
        };
        let index = 0;
        let exists = false;
        marks.forEach((mark, i) => {
            const name = mark.name ?? '';
            if (name === model.component.mark[0].name) {
                index = i;
            }
            else if (name.includes(VORONOI)) {
                exists = true;
            }
        });
        if (!exists) {
            marks.splice(index + 1, 0, cellDef);
        }
        return marks;
    },
};

const inputBindings = {
    defined: (selCmpt) => {
        return (selCmpt.type === 'point' &&
            selCmpt.resolve === 'global' &&
            selCmpt.bind &&
            selCmpt.bind !== 'scales' &&
            !isLegendBinding(selCmpt.bind));
    },
    parse: (model, selCmpt, selDef) => disableDirectManipulation(selCmpt, selDef),
    topLevelSignals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const proj = selCmpt.project;
        const bind = selCmpt.bind;
        const init = selCmpt.init?.[0]; // Can only exist on single selections (one initial value).
        const datum = nearest.defined(selCmpt) ? '(item().isVoronoi ? datum.datum : datum)' : 'datum';
        proj.items.forEach((p, i) => {
            const sgname = varName(`${name}_${p.field}`);
            const hasSignal = signals.filter((s) => s.name === sgname);
            if (!hasSignal.length) {
                signals.unshift({
                    name: sgname,
                    ...(init ? { init: assembleInit(init[i]) } : { value: null }),
                    on: selCmpt.events
                        ? [
                            {
                                events: selCmpt.events,
                                update: `datum && item().mark.marktype !== 'group' ? ${datum}[${$(p.field)}] : null`,
                            },
                        ]
                        : [],
                    bind: bind[p.field] ?? bind[p.channel] ?? bind,
                });
            }
        });
        return signals;
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const proj = selCmpt.project;
        const signal = signals.find((s) => s.name === name + TUPLE);
        const fields = name + TUPLE_FIELDS;
        const values = proj.items.map((p) => varName(`${name}_${p.field}`));
        const valid = values.map((v) => `${v} !== null`).join(' && ');
        if (values.length) {
            signal.update = `${valid} ? {fields: ${fields}, values: [${values.join(', ')}]} : null`;
        }
        delete signal.value;
        delete signal.on;
        return signals;
    },
};

const TOGGLE = '_toggle';
const toggle = {
    defined: (selCmpt) => {
        return selCmpt.type === 'point' && !isTimerSelection(selCmpt) && !!selCmpt.toggle;
    },
    signals: (model, selCmpt, signals) => {
        return signals.concat({
            name: selCmpt.name + TOGGLE,
            value: false,
            on: [{ events: selCmpt.events, update: selCmpt.toggle }],
        });
    },
    modifyExpr: (model, selCmpt) => {
        const tpl = selCmpt.name + TUPLE;
        const signal = selCmpt.name + TOGGLE;
        return `${signal} ? null : ${tpl}, ${selCmpt.resolve === 'global' ? `${signal} ? null : true, ` : `${signal} ? null : {unit: ${unitName(model)}}, `}${signal} ? ${tpl} : null`;
    },
};

const clear = {
    defined: (selCmpt) => {
        return selCmpt.clear !== undefined && selCmpt.clear !== false && !isTimerSelection(selCmpt);
    },
    parse: (model, selCmpt) => {
        if (selCmpt.clear) {
            selCmpt.clear = isString(selCmpt.clear) ? eventSelector(selCmpt.clear, 'view') : selCmpt.clear;
        }
    },
    topLevelSignals: (model, selCmpt, signals) => {
        if (inputBindings.defined(selCmpt)) {
            for (const proj of selCmpt.project.items) {
                const idx = signals.findIndex((n) => n.name === varName(`${selCmpt.name}_${proj.field}`));
                if (idx !== -1) {
                    signals[idx].on.push({ events: selCmpt.clear, update: 'null' });
                }
            }
        }
        return signals;
    },
    signals: (model, selCmpt, signals) => {
        function addClear(idx, update) {
            if (idx !== -1 && signals[idx].on) {
                signals[idx].on.push({ events: selCmpt.clear, update });
            }
        }
        // Be as minimalist as possible when adding clear triggers to minimize dataflow execution.
        if (selCmpt.type === 'interval') {
            for (const proj of selCmpt.project.items) {
                const vIdx = signals.findIndex((n) => n.name === proj.signals.visual);
                addClear(vIdx, '[0, 0]');
                if (vIdx === -1) {
                    const dIdx = signals.findIndex((n) => n.name === proj.signals.data);
                    addClear(dIdx, 'null');
                }
            }
        }
        else {
            let tIdx = signals.findIndex((n) => n.name === selCmpt.name + TUPLE);
            addClear(tIdx, 'null');
            if (toggle.defined(selCmpt)) {
                tIdx = signals.findIndex((n) => n.name === selCmpt.name + TOGGLE);
                addClear(tIdx, 'false');
            }
        }
        return signals;
    },
};

const legendBindings = {
    defined: (selCmpt) => {
        const spec = selCmpt.resolve === 'global' && selCmpt.bind && isLegendBinding(selCmpt.bind);
        const projLen = selCmpt.project.items.length === 1 && selCmpt.project.items[0].field !== SELECTION_ID;
        if (spec && !projLen) {
            warn(LEGEND_BINDINGS_MUST_HAVE_PROJECTION);
        }
        return spec && projLen;
    },
    parse: (model, selCmpt, selDef) => {
        // Allow legend items to be toggleable by default even though direct manipulation is disabled.
        const selDef_ = duplicate(selDef);
        selDef_.select = isString(selDef_.select)
            ? { type: selDef_.select, toggle: selCmpt.toggle }
            : { ...selDef_.select, toggle: selCmpt.toggle };
        disableDirectManipulation(selCmpt, selDef_);
        if (isObject(selDef.select) && (selDef.select.on || selDef.select.clear)) {
            const legendFilter = 'event.item && indexof(event.item.mark.role, "legend") < 0';
            for (const evt of selCmpt.events) {
                evt.filter = array$2(evt.filter ?? []);
                if (!evt.filter.includes(legendFilter)) {
                    evt.filter.push(legendFilter);
                }
            }
        }
        const evt = isLegendStreamBinding(selCmpt.bind) ? selCmpt.bind.legend : 'click';
        const stream = isString(evt) ? eventSelector(evt, 'view') : array$2(evt);
        selCmpt.bind = { legend: { merge: stream } };
    },
    topLevelSignals: (model, selCmpt, signals) => {
        const selName = selCmpt.name;
        const stream = isLegendStreamBinding(selCmpt.bind) && selCmpt.bind.legend;
        const markName = (name) => (s) => {
            const ds = duplicate(s);
            ds.markname = name;
            return ds;
        };
        for (const proj of selCmpt.project.items) {
            if (!proj.hasLegend)
                continue;
            const prefix = `${varName(proj.field)}_legend`;
            const sgName = `${selName}_${prefix}`;
            const hasSignal = signals.filter((s) => s.name === sgName);
            if (hasSignal.length === 0) {
                const events = stream.merge
                    .map(markName(`${prefix}_symbols`))
                    .concat(stream.merge.map(markName(`${prefix}_labels`)))
                    .concat(stream.merge.map(markName(`${prefix}_entries`)));
                signals.unshift({
                    name: sgName,
                    ...(!selCmpt.init ? { value: null } : {}),
                    on: [
                        // Legend entries do not store values, so we need to walk the scenegraph to the symbol datum.
                        {
                            events,
                            update: 'isDefined(datum.value) ? datum.value : item().items[0].items[0].datum.value',
                            force: true,
                        },
                        { events: stream.merge, update: `!event.item || !datum ? null : ${sgName}`, force: true },
                    ],
                });
            }
        }
        return signals;
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const proj = selCmpt.project;
        const tuple = signals.find((s) => s.name === name + TUPLE);
        const fields = name + TUPLE_FIELDS;
        const values = proj.items.filter((p) => p.hasLegend).map((p) => varName(`${name}_${varName(p.field)}_legend`));
        const valid = values.map((v) => `${v} !== null`).join(' && ');
        const update = `${valid} ? {fields: ${fields}, values: [${values.join(', ')}]} : null`;
        if (selCmpt.events && values.length > 0) {
            tuple.on.push({
                events: values.map((signal) => ({ signal })),
                update,
            });
        }
        else if (values.length > 0) {
            tuple.update = update;
            delete tuple.value;
            delete tuple.on;
        }
        const toggle = signals.find((s) => s.name === name + TOGGLE);
        const events = isLegendStreamBinding(selCmpt.bind) && selCmpt.bind.legend;
        if (toggle) {
            if (!selCmpt.events)
                toggle.on[0].events = events;
            else
                toggle.on.push({ ...toggle.on[0], events });
        }
        return signals;
    },
};
function parseInteractiveLegend(model, channel, legendCmpt) {
    const field = model.fieldDef(channel)?.field;
    for (const selCmpt of vals(model.component.selection ?? {})) {
        const proj = selCmpt.project.hasField[field] ?? selCmpt.project.hasChannel[channel];
        if (proj && legendBindings.defined(selCmpt)) {
            const legendSelections = legendCmpt.get('selections') ?? [];
            legendSelections.push(selCmpt.name);
            legendCmpt.set('selections', legendSelections, false);
            proj.hasLegend = true;
        }
    }
}

const ANCHOR$1 = '_translate_anchor';
const DELTA$1 = '_translate_delta';
const translate = {
    defined: (selCmpt) => {
        return selCmpt.type === 'interval' && selCmpt.translate;
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const boundScales = scaleBindings.defined(selCmpt);
        const anchor = name + ANCHOR$1;
        const { x, y } = selCmpt.project.hasChannel;
        let events = eventSelector(selCmpt.translate, 'scope');
        if (!boundScales) {
            events = events.map((e) => ((e.between[0].markname = name + BRUSH), e));
        }
        signals.push({
            name: anchor,
            value: {},
            on: [
                {
                    events: events.map((e) => e.between[0]),
                    update: `{x: x(unit), y: y(unit)${x !== undefined ? `, extent_x: ${boundScales ? domain(model, X) : `slice(${x.signals.visual})`}` : ''}${y !== undefined ? `, extent_y: ${boundScales ? domain(model, Y) : `slice(${y.signals.visual})`}` : ''}}`,
                },
            ],
        }, {
            name: name + DELTA$1,
            value: {},
            on: [
                {
                    events,
                    update: `{x: ${anchor}.x - x(unit), y: ${anchor}.y - y(unit)}`,
                },
            ],
        });
        if (x !== undefined) {
            onDelta$1(model, selCmpt, x, 'width', signals);
        }
        if (y !== undefined) {
            onDelta$1(model, selCmpt, y, 'height', signals);
        }
        return signals;
    },
};
function onDelta$1(model, selCmpt, proj, size, signals) {
    const name = selCmpt.name;
    const anchor = name + ANCHOR$1;
    const delta = name + DELTA$1;
    const channel = proj.channel;
    const boundScales = scaleBindings.defined(selCmpt);
    const signal = signals.find((s) => s.name === proj.signals[boundScales ? 'data' : 'visual']);
    const sizeSg = model.getSizeSignalRef(size).signal;
    const scaleCmpt = model.getScaleComponent(channel);
    const scaleType = scaleCmpt?.get('type');
    const reversed = scaleCmpt?.get('reverse'); // scale parsing sets this flag for fieldDef.sort
    const sign = !boundScales ? '' : channel === X ? (reversed ? '' : '-') : reversed ? '-' : '';
    const extent = `${anchor}.extent_${channel}`;
    const offset = `${sign}${delta}.${channel} / ${boundScales ? `${sizeSg}` : `span(${extent})`}`;
    const panFn = !boundScales || !scaleCmpt
        ? 'panLinear'
        : scaleType === 'log'
            ? 'panLog'
            : scaleType === 'symlog'
                ? 'panSymlog'
                : scaleType === 'pow'
                    ? 'panPow'
                    : 'panLinear';
    const arg = !boundScales
        ? ''
        : scaleType === 'pow'
            ? `, ${scaleCmpt.get('exponent') ?? 1}`
            : scaleType === 'symlog'
                ? `, ${scaleCmpt.get('constant') ?? 1}`
                : '';
    const update = `${panFn}(${extent}, ${offset}${arg})`;
    signal.on.push({
        events: { signal: delta },
        update: boundScales ? update : `clampRange(${update}, 0, ${sizeSg})`,
    });
}

const ANCHOR = '_zoom_anchor';
const DELTA = '_zoom_delta';
const zoom = {
    defined: (selCmpt) => {
        return selCmpt.type === 'interval' && selCmpt.zoom;
    },
    signals: (model, selCmpt, signals) => {
        const name = selCmpt.name;
        const boundScales = scaleBindings.defined(selCmpt);
        const delta = name + DELTA;
        const { x, y } = selCmpt.project.hasChannel;
        const sx = $(model.scaleName(X));
        const sy = $(model.scaleName(Y));
        let events = eventSelector(selCmpt.zoom, 'scope');
        if (!boundScales) {
            events = events.map((e) => ((e.markname = name + BRUSH), e));
        }
        signals.push({
            name: name + ANCHOR,
            on: [
                {
                    events,
                    update: !boundScales
                        ? `{x: x(unit), y: y(unit)}`
                        : `{${[sx ? `x: invert(${sx}, x(unit))` : '', sy ? `y: invert(${sy}, y(unit))` : '']
                            .filter((expr) => expr)
                            .join(', ')}}`,
                },
            ],
        }, {
            name: delta,
            on: [
                {
                    events,
                    force: true,
                    update: 'pow(1.001, event.deltaY * pow(16, event.deltaMode))',
                },
            ],
        });
        if (x !== undefined) {
            onDelta(model, selCmpt, x, 'width', signals);
        }
        if (y !== undefined) {
            onDelta(model, selCmpt, y, 'height', signals);
        }
        return signals;
    },
};
function onDelta(model, selCmpt, proj, size, signals) {
    const name = selCmpt.name;
    const channel = proj.channel;
    const boundScales = scaleBindings.defined(selCmpt);
    const signal = signals.find((s) => s.name === proj.signals[boundScales ? 'data' : 'visual']);
    const sizeSg = model.getSizeSignalRef(size).signal;
    const scaleCmpt = model.getScaleComponent(channel);
    const scaleType = scaleCmpt?.get('type');
    const base = boundScales ? domain(model, channel) : signal.name;
    const delta = name + DELTA;
    const anchor = `${name}${ANCHOR}.${channel}`;
    const zoomFn = !boundScales || !scaleCmpt
        ? 'zoomLinear'
        : scaleType === 'log'
            ? 'zoomLog'
            : scaleType === 'symlog'
                ? 'zoomSymlog'
                : scaleType === 'pow'
                    ? 'zoomPow'
                    : 'zoomLinear';
    const arg = !boundScales
        ? ''
        : scaleType === 'pow'
            ? `, ${scaleCmpt.get('exponent') ?? 1}`
            : scaleType === 'symlog'
                ? `, ${scaleCmpt.get('constant') ?? 1}`
                : '';
    const update = `${zoomFn}(${base}, ${anchor}, ${delta}${arg})`;
    signal.on.push({
        events: { signal: delta },
        update: boundScales ? update : `clampRange(${update}, 0, ${sizeSg})`,
    });
}

const STORE = '_store';
const TUPLE = '_tuple';
const MODIFY = '_modify';
const VL_SELECTION_RESOLVE = 'vlSelectionResolve';
// Order matters for parsing and assembly.
const selectionCompilers = [
    point$1,
    interval,
    project,
    toggle,
    // Bindings may disable direct manipulation.
    inputBindings,
    scaleBindings,
    legendBindings,
    clear,
    translate,
    zoom,
    nearest,
];
function getFacetModel(model) {
    let parent = model.parent;
    while (parent) {
        if (isFacetModel(parent))
            break;
        parent = parent.parent;
    }
    return parent;
}
function unitName(model, { escape } = { escape: true }) {
    let name = escape ? $(model.name) : model.name;
    const facetModel = getFacetModel(model);
    if (facetModel) {
        const { facet } = facetModel;
        for (const channel of FACET_CHANNELS) {
            if (facet[channel]) {
                name += ` + '__facet_${channel}_' + (facet[${$(facetModel.vgField(channel))}])`;
            }
        }
    }
    return name;
}
function requiresSelectionId(model) {
    return vals(model.component.selection ?? {}).reduce((identifier, selCmpt) => {
        return identifier || selCmpt.project.hasSelectionId;
    }, false);
}
// Binding a point selection to query widgets or legends disables default direct manipulation interaction.
// A user can choose to re-enable it by explicitly specifying triggering input events.
function disableDirectManipulation(selCmpt, selDef) {
    if (isString(selDef.select) || !selDef.select.on)
        delete selCmpt.events;
    if (isString(selDef.select) || !selDef.select.clear)
        delete selCmpt.clear;
    if (isString(selDef.select) || !selDef.select.toggle)
        delete selCmpt.toggle;
}
function isTimerSelection(selCmpt) {
    return selCmpt.events?.find((e) => 'type' in e && e.type === 'timer');
}

function getName(node) {
    const name = [];
    if (node.type === 'Identifier') {
        return [node.name];
    }
    if (node.type === 'Literal') {
        return [node.value];
    }
    if (node.type === 'MemberExpression') {
        name.push(...getName(node.object));
        name.push(...getName(node.property));
    }
    return name;
}
function startsWithDatum(node) {
    if (node.object.type === 'MemberExpression') {
        return startsWithDatum(node.object);
    }
    return node.object.name === 'datum';
}
function getDependentFields(expression) {
    const ast = parser$1(expression);
    const dependents = new Set();
    // visit is missing in types https://github.com/vega/vega/issues/3298
    ast.visit((node) => {
        if (node.type === 'MemberExpression' && startsWithDatum(node)) {
            dependents.add(getName(node).slice(1).join('.'));
        }
    });
    return dependents;
}

class FilterNode extends DataFlowNode {
    model;
    filter;
    expr;
    _dependentFields;
    clone() {
        return new FilterNode(null, this.model, duplicate(this.filter));
    }
    constructor(parent, model, filter) {
        super(parent);
        this.model = model;
        this.filter = filter;
        // TODO: refactor this to not take a node and
        // then add a static function makeFromOperand and make the constructor take only an expression
        this.expr = expression(this.model, this.filter, this);
        this._dependentFields = getDependentFields(this.expr);
    }
    dependentFields() {
        return this._dependentFields;
    }
    producedFields() {
        return new Set(); // filter does not produce any new fields
    }
    assemble() {
        return {
            type: 'filter',
            expr: this.expr,
        };
    }
    hash() {
        return `Filter ${this.expr}`;
    }
}

function parseUnitSelection(model, selDefs) {
    const selCmpts = {};
    const selectionConfig = model.config.selection;
    if (!selDefs || !selDefs.length)
        return selCmpts;
    let nTimerSelections = 0;
    for (const def of selDefs) {
        const name = varName(def.name);
        const selDef = def.select;
        const type = isString(selDef) ? selDef : selDef.type;
        const defaults = isObject(selDef) ? duplicate(selDef) : { type };
        // Set default values from config if a property hasn't been specified,
        // or if it is true. E.g., "translate": true should use the default
        // event handlers for translate. However, true may be a valid value for
        // a property (e.g., "nearest": true).
        const cfg = selectionConfig[type];
        for (const key in cfg) {
            // Project transform applies its defaults.
            if (key === 'fields' || key === 'encodings') {
                continue;
            }
            if (key === 'mark') {
                defaults.mark = { ...cfg.mark, ...defaults.mark };
            }
            if (defaults[key] === undefined || defaults[key] === true) {
                defaults[key] = duplicate(cfg[key] ?? defaults[key]);
            }
        }
        const selCmpt = (selCmpts[name] = {
            ...defaults,
            name,
            type,
            init: def.value,
            bind: def.bind,
            events: isString(defaults.on) ? eventSelector(defaults.on, 'scope') : array$2(duplicate(defaults.on)),
        });
        if (isTimerSelection(selCmpt)) {
            nTimerSelections++;
            // check for multiple timer selections and ignore all but the first one
            if (nTimerSelections > 1) {
                delete selCmpts[name];
                continue;
            }
        }
        const def_ = duplicate(def); // defensive copy to prevent compilers from causing side effects
        for (const c of selectionCompilers) {
            if (c.defined(selCmpt) && c.parse) {
                c.parse(model, selCmpt, def_);
            }
        }
    }
    if (nTimerSelections > 1) {
        // if multiple timer selections were found, issue a warning
        warn(MULTIPLE_TIMER_ANIMATION_SELECTION);
    }
    return selCmpts;
}
function parseSelectionPredicate(model, pred, dfnode, datum = 'datum') {
    const name = isString(pred) ? pred : pred.param;
    const vname = varName(name);
    const store = $(vname + STORE);
    let selCmpt;
    try {
        selCmpt = model.getSelectionComponent(vname, name);
    }
    catch {
        // If a selection isn't found, treat as a variable parameter and coerce to boolean.
        return `!!${vname}`;
    }
    if (selCmpt.project.timeUnit) {
        const child = dfnode ?? model.component.data.raw;
        const tunode = selCmpt.project.timeUnit.clone();
        if (child.parent) {
            tunode.insertAsParentOf(child);
        }
        else {
            child.parent = tunode;
        }
    }
    const fn = selCmpt.project.hasSelectionId ? 'vlSelectionIdTest(' : 'vlSelectionTest(';
    const resolve = selCmpt.resolve === 'global' ? ')' : `, ${$(selCmpt.resolve)})`;
    const test = `${fn}${store}, ${datum}${resolve}`;
    const length = `length(data(${store}))`;
    return pred.empty === false ? `${length} && ${test}` : `!${length} || ${test}`;
}
function parseSelectionExtent(model, name, extent) {
    const vname = varName(name);
    const encoding = extent.encoding;
    let field = extent.field;
    let selCmpt;
    try {
        selCmpt = model.getSelectionComponent(vname, name);
    }
    catch {
        // If a selection isn't found, treat it as a variable parameter.
        return vname;
    }
    if (!encoding && !field) {
        field = selCmpt.project.items[0].field;
        if (selCmpt.project.items.length > 1) {
            warn(selectionAsScaleDomainWithoutField(field));
        }
    }
    else if (encoding && !field) {
        const encodings = selCmpt.project.items.filter((p) => p.channel === encoding);
        if (!encodings.length || encodings.length > 1) {
            field = selCmpt.project.items[0].field;
            warn(selectionAsScaleDomainWrongEncodings(encodings, encoding, extent, field));
        }
        else {
            field = encodings[0].field;
        }
    }
    return `${selCmpt.name}[${$(replacePathInField(field))}]`;
}
function materializeSelections(model, main) {
    for (const [selection, selCmpt] of entries$1(model.component.selection ?? {})) {
        const lookupName = model.getName(`lookup_${selection}`);
        model.component.data.outputNodes[lookupName] = selCmpt.materialized = new OutputNode(new FilterNode(main, model, { param: selection }), lookupName, DataSourceType.Lookup, model.component.data.outputNodeRefCounts);
    }
}

/**
 * Converts a predicate into an expression.
 */
// model is only used for selection filters.
function expression(model, filterOp, node) {
    return logicalExpr(filterOp, (predicate) => {
        if (isString(predicate)) {
            return predicate;
        }
        else if (isSelectionPredicate(predicate)) {
            return parseSelectionPredicate(model, predicate, node);
        }
        else {
            // Filter Object
            return fieldFilterExpression(predicate);
        }
    });
}

function assembleTitle(title, config) {
    if (!title) {
        return undefined;
    }
    if (isArray(title) && !isText(title)) {
        return title.map((fieldDef) => defaultTitle(fieldDef, config)).join(', ');
    }
    return title;
}
function setAxisEncode(axis, part, vgProp, vgRef) {
    axis.encode ??= {};
    axis.encode[part] ??= {};
    axis.encode[part].update ??= {};
    // TODO: remove as any after https://github.com/prisma/nexus-prisma/issues/291
    axis.encode[part].update[vgProp] = vgRef;
}
function assembleAxis(axisCmpt, kind, config, opt = { header: false }) {
    const { disable, orient, scale, labelExpr, title, zindex, ...axis } = axisCmpt.combine();
    if (disable) {
        return undefined;
    }
    for (const p in axis) {
        const prop = p;
        const propType = AXIS_PROPERTY_TYPE[prop];
        const propValue = axis[prop];
        if (propType && propType !== kind && propType !== 'both') {
            // Remove properties that are not valid for this kind of axis
            delete axis[prop];
        }
        else if (isConditionalAxisValue(propValue)) {
            // deal with conditional axis value
            const { condition, ...valueOrSignalRef } = propValue;
            const conditions = array$2(condition);
            const propIndex = CONDITIONAL_AXIS_PROP_INDEX[prop];
            if (propIndex) {
                const { vgProp, part } = propIndex;
                // If there is a corresponding Vega property for the channel,
                // use Vega's custom axis encoding and delete the original axis property to avoid conflicts
                const vgRef = [
                    ...conditions.map((c) => {
                        const { test, ...valueOrSignalCRef } = c;
                        return {
                            test: expression(null, test),
                            ...valueOrSignalCRef,
                        };
                    }),
                    valueOrSignalRef,
                ];
                setAxisEncode(axis, part, vgProp, vgRef);
                delete axis[prop];
            }
            else if (propIndex === null) {
                // If propIndex is null, this means we support conditional axis property by converting the condition to signal instead.
                const signalRef = {
                    signal: conditions
                        .map((c) => {
                        const { test, ...valueOrSignalCRef } = c;
                        return `${expression(null, test)} ? ${exprFromValueRefOrSignalRef(valueOrSignalCRef)} : `;
                    })
                        .join('') + exprFromValueRefOrSignalRef(valueOrSignalRef),
                };
                axis[prop] = signalRef;
            }
        }
        else if (isSignalRef(propValue)) {
            const propIndex = CONDITIONAL_AXIS_PROP_INDEX[prop];
            if (propIndex) {
                const { vgProp, part } = propIndex;
                // FIXME: remove as any
                setAxisEncode(axis, part, vgProp, propValue);
                delete axis[prop];
            } // else do nothing since the property already supports signal
        }
        // Do not pass labelAlign/Baseline = null to Vega since it won't pass the schema
        // Note that we need to use null so the default labelAlign is preserved.
        if (contains(['labelAlign', 'labelBaseline'], prop) && axis[prop] === null) {
            delete axis[prop];
        }
    }
    if (kind === 'grid') {
        if (!axis.grid) {
            return undefined;
        }
        // Remove unnecessary encode block
        if (axis.encode) {
            // Only need to keep encode block for grid
            const { grid } = axis.encode;
            axis.encode = {
                ...(grid ? { grid } : {}),
            };
            if (isEmpty(axis.encode)) {
                delete axis.encode;
            }
        }
        return {
            scale,
            orient,
            ...axis,
            domain: false,
            labels: false,
            aria: false, // always hide grid axis
            // Always set min/maxExtent to 0 to ensure that `config.axis*.minExtent` and `config.axis*.maxExtent`
            // would not affect gridAxis
            maxExtent: 0,
            minExtent: 0,
            ticks: false,
            zindex: getFirstDefined(zindex, 0), // put grid behind marks by default
        };
    }
    else {
        // kind === 'main'
        if (!opt.header && axisCmpt.mainExtracted) {
            // if mainExtracted has been extracted to a separate facet
            return undefined;
        }
        if (labelExpr !== undefined) {
            let expr = labelExpr;
            if (axis.encode?.labels?.update && isSignalRef(axis.encode.labels.update.text)) {
                expr = replaceAll(labelExpr, 'datum.label', axis.encode.labels.update.text.signal);
            }
            setAxisEncode(axis, 'labels', 'text', { signal: expr });
        }
        if (axis.labelAlign === null) {
            delete axis.labelAlign;
        }
        // Remove unnecessary encode block
        if (axis.encode) {
            for (const part of AXIS_PARTS) {
                if (!axisCmpt.hasAxisPart(part)) {
                    delete axis.encode[part];
                }
            }
            if (isEmpty(axis.encode)) {
                delete axis.encode;
            }
        }
        const titleString = assembleTitle(title, config);
        return {
            scale,
            orient,
            grid: false,
            ...(titleString ? { title: titleString } : {}),
            ...axis,
            ...(config.aria === false ? { aria: false } : {}),
            zindex: getFirstDefined(zindex, 0), // put axis line above marks by default
        };
    }
}
/**
 * Add axis signals so grid line works correctly
 * (Fix https://github.com/vega/vega-lite/issues/4226)
 */
function assembleAxisSignals(model) {
    const { axes } = model.component;
    const signals = [];
    for (const channel of POSITION_SCALE_CHANNELS) {
        if (axes[channel]) {
            for (const axis of axes[channel]) {
                if (!axis.get('disable') && !axis.get('gridScale')) {
                    // If there is x-axis but no y-scale for gridScale, need to set height/width so x-axis can draw the grid with the right height. Same for y-axis and width.
                    const sizeType = channel === 'x' ? 'height' : 'width';
                    const update = model.getSizeSignalRef(sizeType).signal;
                    if (sizeType !== update) {
                        signals.push({
                            name: sizeType,
                            update,
                        });
                    }
                }
            }
        }
    }
    return signals;
}
function assembleAxes(axisComponents, config) {
    const { x = [], y = [] } = axisComponents;
    return [
        ...x.map((a) => assembleAxis(a, 'grid', config)),
        ...y.map((a) => assembleAxis(a, 'grid', config)),
        ...x.map((a) => assembleAxis(a, 'main', config)),
        ...y.map((a) => assembleAxis(a, 'main', config)),
    ].filter((a) => a); // filter undefined
}

function getAxisConfigFromConfigTypes(configTypes, config, channel, orient) {
    // TODO: add special casing to add conditional value based on orient signal
    return Object.assign.apply(null, [
        {},
        ...configTypes.map((configType) => {
            if (configType === 'axisOrient') {
                const orient1 = channel === 'x' ? 'bottom' : 'left';
                const orientConfig1 = config[channel === 'x' ? 'axisBottom' : 'axisLeft'] || {};
                const orientConfig2 = config[channel === 'x' ? 'axisTop' : 'axisRight'] || {};
                const props = new Set([...keys(orientConfig1), ...keys(orientConfig2)]);
                const conditionalOrientAxisConfig = {};
                for (const prop of props.values()) {
                    conditionalOrientAxisConfig[prop] = {
                        // orient is surely signal in this case
                        signal: `${orient['signal']} === "${orient1}" ? ${signalOrStringValue(orientConfig1[prop])} : ${signalOrStringValue(orientConfig2[prop])}`,
                    };
                }
                return conditionalOrientAxisConfig;
            }
            return config[configType];
        }),
    ]);
}
function getAxisConfigs(channel, scaleType, orient, config) {
    const typeBasedConfigTypes = scaleType === 'band'
        ? ['axisDiscrete', 'axisBand']
        : scaleType === 'point'
            ? ['axisDiscrete', 'axisPoint']
            : isQuantitative(scaleType)
                ? ['axisQuantitative']
                : scaleType === 'time' || scaleType === 'utc'
                    ? ['axisTemporal']
                    : [];
    const axisChannel = channel === 'x' ? 'axisX' : 'axisY';
    const axisOrient = isSignalRef(orient) ? 'axisOrient' : `axis${titleCase(orient)}`; // axisTop, axisBottom, ...
    const vlOnlyConfigTypes = [
        // technically Vega does have axisBand, but if we make another separation here,
        // it will further introduce complexity in the code
        ...typeBasedConfigTypes,
        ...typeBasedConfigTypes.map((c) => axisChannel + c.substr(4)),
    ];
    const vgConfigTypes = ['axis', axisOrient, axisChannel];
    return {
        vlOnlyAxisConfig: getAxisConfigFromConfigTypes(vlOnlyConfigTypes, config, channel, orient),
        vgAxisConfig: getAxisConfigFromConfigTypes(vgConfigTypes, config, channel, orient),
        axisConfigStyle: getAxisConfigStyle([...vgConfigTypes, ...vlOnlyConfigTypes], config),
    };
}
function getAxisConfigStyle(axisConfigTypes, config) {
    const toMerge = [{}];
    for (const configType of axisConfigTypes) {
        // TODO: add special casing to add conditional value based on orient signal
        let style = config[configType]?.style;
        if (style) {
            style = array$2(style);
            for (const s of style) {
                toMerge.push(config.style[s]);
            }
        }
    }
    return Object.assign.apply(null, toMerge);
}
function getAxisConfig(property, styleConfigIndex, style, axisConfigs = {}) {
    const styleConfig = getStyleConfig(property, style, styleConfigIndex);
    if (styleConfig !== undefined) {
        return {
            configFrom: 'style',
            configValue: styleConfig,
        };
    }
    for (const configFrom of ['vlOnlyAxisConfig', 'vgAxisConfig', 'axisConfigStyle']) {
        if (axisConfigs[configFrom]?.[property] !== undefined) {
            return { configFrom, configValue: axisConfigs[configFrom][property] };
        }
    }
    return {};
}

const axisRules = {
    scale: ({ model, channel }) => model.scaleName(channel),
    format: ({ format }) => format, // we already calculate this in parse
    formatType: ({ formatType }) => formatType, // we already calculate this in parse
    grid: ({ fieldOrDatumDef, axis, scaleType }) => axis.grid ?? defaultGrid(scaleType, fieldOrDatumDef),
    gridScale: ({ model, channel }) => gridScale(model, channel),
    labelAlign: ({ axis, labelAngle, orient, channel }) => axis.labelAlign || defaultLabelAlign(labelAngle, orient, channel),
    labelAngle: ({ labelAngle }) => labelAngle, // we already calculate this in parse
    labelBaseline: ({ axis, labelAngle, orient, channel }) => axis.labelBaseline || defaultLabelBaseline(labelAngle, orient, channel),
    labelFlush: ({ axis, fieldOrDatumDef, channel }) => axis.labelFlush ?? defaultLabelFlush(fieldOrDatumDef.type, channel),
    labelOverlap: ({ axis, fieldOrDatumDef, scaleType }) => axis.labelOverlap ??
        defaultLabelOverlap$1(fieldOrDatumDef.type, scaleType, isFieldDef(fieldOrDatumDef) && !!fieldOrDatumDef.timeUnit, isFieldDef(fieldOrDatumDef) ? fieldOrDatumDef.sort : undefined),
    // we already calculate orient in parse
    orient: ({ orient }) => orient, // Need to cast until Vega supports signal
    tickCount: ({ channel, model, axis, fieldOrDatumDef, scaleType }) => {
        const sizeType = channel === 'x' ? 'width' : channel === 'y' ? 'height' : undefined;
        const size = sizeType ? model.getSizeSignalRef(sizeType) : undefined;
        return axis.tickCount ?? defaultTickCount({ fieldOrDatumDef, scaleType, size, values: axis.values });
    },
    tickMinStep: ({ axis, format, fieldOrDatumDef }) => axis.tickMinStep ?? defaultTickMinStep({ format, fieldOrDatumDef }),
    title: ({ axis, model, channel }) => {
        if (axis.title !== undefined) {
            return axis.title;
        }
        const fieldDefTitle = getFieldDefTitle(model, channel);
        if (fieldDefTitle !== undefined) {
            return fieldDefTitle;
        }
        const fieldDef = model.typedFieldDef(channel);
        const channel2 = channel === 'x' ? 'x2' : 'y2';
        const fieldDef2 = model.fieldDef(channel2);
        // If title not specified, store base parts of fieldDef (and fieldDef2 if exists)
        return mergeTitleFieldDefs(fieldDef ? [toFieldDefBase(fieldDef)] : [], isFieldDef(fieldDef2) ? [toFieldDefBase(fieldDef2)] : []);
    },
    values: ({ axis, fieldOrDatumDef }) => values$1(axis, fieldOrDatumDef),
    zindex: ({ axis, fieldOrDatumDef, mark }) => axis.zindex ?? defaultZindex(mark, fieldOrDatumDef),
};
// TODO: we need to refactor this method after we take care of config refactoring
/**
 * Default rules for whether to show a grid should be shown for a channel.
 * If `grid` is unspecified, the default value is `true` for ordinal scales that are not binned
 */
function defaultGrid(scaleType, fieldDef) {
    return !hasDiscreteDomain(scaleType) && isFieldDef(fieldDef) && !isBinning(fieldDef?.bin) && !isBinned(fieldDef?.bin);
}
function gridScale(model, channel) {
    const gridChannel = channel === 'x' ? 'y' : 'x';
    if (model.getScaleComponent(gridChannel)) {
        return model.scaleName(gridChannel);
    }
    return undefined;
}
function getLabelAngle(fieldOrDatumDef, axis, channel, styleConfig, axisConfigs) {
    const labelAngle = axis?.labelAngle;
    // try axis value
    if (labelAngle !== undefined) {
        return isSignalRef(labelAngle) ? labelAngle : normalizeAngle(labelAngle);
    }
    else {
        // try axis config value
        const { configValue: angle } = getAxisConfig('labelAngle', styleConfig, axis?.style, axisConfigs);
        if (angle !== undefined) {
            return normalizeAngle(angle);
        }
        else {
            // get default value
            if (channel === X &&
                contains([NOMINAL, ORDINAL], fieldOrDatumDef.type) &&
                !(isFieldDef(fieldOrDatumDef) && fieldOrDatumDef.timeUnit)) {
                return 270;
            }
            // no default
            return undefined;
        }
    }
}
function normalizeAngleExpr(angle) {
    return `(((${angle.signal} % 360) + 360) % 360)`;
}
function defaultLabelBaseline(angle, orient, channel, alwaysIncludeMiddle) {
    if (angle !== undefined) {
        if (channel === 'x') {
            if (isSignalRef(angle)) {
                const a = normalizeAngleExpr(angle);
                const orientIsTop = isSignalRef(orient) ? `(${orient.signal} === "top")` : orient === 'top';
                return {
                    signal: `(45 < ${a} && ${a} < 135) || (225 < ${a} && ${a} < 315) ? "middle" :` +
                        `(${a} <= 45 || 315 <= ${a}) === ${orientIsTop} ? "bottom" : "top"`,
                };
            }
            if ((45 < angle && angle < 135) || (225 < angle && angle < 315)) {
                return 'middle';
            }
            if (isSignalRef(orient)) {
                const op = angle <= 45 || 315 <= angle ? '===' : '!==';
                return { signal: `${orient.signal} ${op} "top" ? "bottom" : "top"` };
            }
            return (angle <= 45 || 315 <= angle) === (orient === 'top') ? 'bottom' : 'top';
        }
        else {
            if (isSignalRef(angle)) {
                const a = normalizeAngleExpr(angle);
                const orientIsLeft = isSignalRef(orient) ? `(${orient.signal} === "left")` : orient === 'left';
                const middle = alwaysIncludeMiddle ? '"middle"' : 'null';
                return {
                    signal: `${a} <= 45 || 315 <= ${a} || (135 <= ${a} && ${a} <= 225) ? ${middle} : (45 <= ${a} && ${a} <= 135) === ${orientIsLeft} ? "top" : "bottom"`,
                };
            }
            if (angle <= 45 || 315 <= angle || (135 <= angle && angle <= 225)) {
                return alwaysIncludeMiddle ? 'middle' : null;
            }
            if (isSignalRef(orient)) {
                const op = 45 <= angle && angle <= 135 ? '===' : '!==';
                return { signal: `${orient.signal} ${op} "left" ? "top" : "bottom"` };
            }
            return (45 <= angle && angle <= 135) === (orient === 'left') ? 'top' : 'bottom';
        }
    }
    return undefined;
}
function defaultLabelAlign(angle, orient, channel) {
    if (angle === undefined) {
        return undefined;
    }
    const isX = channel === 'x';
    const startAngle = isX ? 0 : 90;
    const mainOrient = isX ? 'bottom' : 'left';
    if (isSignalRef(angle)) {
        const a = normalizeAngleExpr(angle);
        const orientIsMain = isSignalRef(orient) ? `(${orient.signal} === "${mainOrient}")` : orient === mainOrient;
        return {
            signal: `(${startAngle ? `(${a} + 90)` : a} % 180 === 0) ? ${isX ? null : '"center"'} :` +
                `(${startAngle} < ${a} && ${a} < ${180 + startAngle}) === ${orientIsMain} ? "left" : "right"`,
        };
    }
    if ((angle + startAngle) % 180 === 0) {
        // For bottom, use default label align so label flush still works
        return isX ? null : 'center';
    }
    if (isSignalRef(orient)) {
        const op = startAngle < angle && angle < 180 + startAngle ? '===' : '!==';
        const orientIsMain = `${orient.signal} ${op} "${mainOrient}"`;
        return {
            signal: `${orientIsMain} ? "left" : "right"`,
        };
    }
    if ((startAngle < angle && angle < 180 + startAngle) === (orient === mainOrient)) {
        return 'left';
    }
    return 'right';
}
function defaultLabelFlush(type, channel) {
    if (channel === 'x' && contains(['quantitative', 'temporal'], type)) {
        return true;
    }
    return undefined;
}
function defaultLabelOverlap$1(type, scaleType, hasTimeUnit, sort) {
    // do not prevent overlap for nominal data because there is no way to infer what the missing labels are
    if ((hasTimeUnit && !isObject(sort)) || (type !== 'nominal' && type !== 'ordinal')) {
        if (scaleType === 'log' || scaleType === 'symlog') {
            return 'greedy';
        }
        return true;
    }
    return undefined;
}
function defaultOrient(channel) {
    return channel === 'x' ? 'bottom' : 'left';
}
function defaultTickCount({ fieldOrDatumDef, scaleType, size, values: vals, }) {
    if (!vals && !hasDiscreteDomain(scaleType) && scaleType !== 'log') {
        if (isFieldDef(fieldOrDatumDef)) {
            if (isBinning(fieldOrDatumDef.bin)) {
                // for binned data, we don't want more ticks than maxbins
                return { signal: `ceil(${size.signal}/10)` };
            }
            if (fieldOrDatumDef.timeUnit &&
                contains(['month', 'hours', 'day', 'quarter'], normalizeTimeUnit(fieldOrDatumDef.timeUnit)?.unit)) {
                return undefined;
            }
        }
        return { signal: `ceil(${size.signal}/40)` };
    }
    return undefined;
}
function defaultTickMinStep({ format, fieldOrDatumDef }) {
    if (format === 'd') {
        return 1;
    }
    if (isFieldDef(fieldOrDatumDef)) {
        const { timeUnit } = fieldOrDatumDef;
        if (timeUnit) {
            const signal = durationExpr(timeUnit);
            if (signal) {
                return { signal };
            }
        }
    }
    return undefined;
}
function getFieldDefTitle(model, channel) {
    const channel2 = channel === 'x' ? 'x2' : 'y2';
    const fieldDef = model.fieldDef(channel);
    const fieldDef2 = model.fieldDef(channel2);
    const title1 = fieldDef ? fieldDef.title : undefined;
    const title2 = fieldDef2 ? fieldDef2.title : undefined;
    if (title1 && title2) {
        return mergeTitle(title1, title2);
    }
    else if (title1) {
        return title1;
    }
    else if (title2) {
        return title2;
    }
    else if (title1 !== undefined) {
        // falsy value to disable config
        return title1;
    }
    else if (title2 !== undefined) {
        // falsy value to disable config
        return title2;
    }
    return undefined;
}
function values$1(axis, fieldOrDatumDef) {
    const vals = axis.values;
    if (isArray(vals)) {
        return valueArray(fieldOrDatumDef, vals);
    }
    else if (isSignalRef(vals)) {
        return vals;
    }
    return undefined;
}
function defaultZindex(mark, fieldDef) {
    if (mark === 'rect' && isDiscrete(fieldDef)) {
        return 1;
    }
    return 0;
}

class CalculateNode extends DataFlowNode {
    transform;
    _dependentFields;
    clone() {
        return new CalculateNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this._dependentFields = getDependentFields(this.transform.calculate);
    }
    static parseAllForSortIndex(parent, model) {
        // get all the encoding with sort fields from model
        model.forEachFieldDef((fieldDef, channel) => {
            if (!isScaleFieldDef(fieldDef)) {
                return;
            }
            if (isSortArray(fieldDef.sort)) {
                const { field, timeUnit } = fieldDef;
                const sort = fieldDef.sort;
                // generate `datum["a"] === val0 ? 0 : datum["a"] === val1 ? 1 : ... : n` via FieldEqualPredicate
                const calculate = sort
                    .map((sortValue, i) => {
                    return `${fieldFilterExpression({ field, timeUnit, equal: sortValue })} ? ${i} : `;
                })
                    .join('') + sort.length;
                parent = new CalculateNode(parent, {
                    calculate,
                    as: sortArrayIndexField(fieldDef, channel, { forAs: true }),
                });
            }
        });
        return parent;
    }
    producedFields() {
        return new Set([this.transform.as]);
    }
    dependentFields() {
        return this._dependentFields;
    }
    assemble() {
        return {
            type: 'formula',
            expr: this.transform.calculate,
            as: this.transform.as,
        };
    }
    hash() {
        return `Calculate ${hash(this.transform)}`;
    }
}
function sortArrayIndexField(fieldDef, channel, opt) {
    return vgField(fieldDef, { prefix: channel, suffix: 'sort_index', ...opt });
}

/**
 * Get header channel, which can be different from facet channel when orient is specified or when the facet channel is facet.
 */
function getHeaderChannel(channel, orient) {
    if (contains(['top', 'bottom'], orient)) {
        return 'column';
    }
    else if (contains(['left', 'right'], orient)) {
        return 'row';
    }
    return channel === 'row' ? 'row' : 'column';
}
function getHeaderProperty(prop, header, config, channel) {
    const headerSpecificConfig = channel === 'row' ? config.headerRow : channel === 'column' ? config.headerColumn : config.headerFacet;
    return getFirstDefined((header || {})[prop], headerSpecificConfig[prop], config.header[prop]);
}
function getHeaderProperties(properties, header, config, channel) {
    const props = {};
    for (const prop of properties) {
        const value = getHeaderProperty(prop, header || {}, config, channel);
        if (value !== undefined) {
            props[prop] = value;
        }
    }
    return props;
}

const HEADER_CHANNELS = ['row', 'column'];
const HEADER_TYPES = ['header', 'footer'];

/**
 * Utility for generating row / column headers
 */
// TODO: rename to assembleHeaderTitleGroup
function assembleTitleGroup(model, channel) {
    const title = model.component.layoutHeaders[channel].title;
    const config = model.config ? model.config : undefined;
    const facetFieldDef = model.component.layoutHeaders[channel].facetFieldDef
        ? model.component.layoutHeaders[channel].facetFieldDef
        : undefined;
    const { titleAnchor, titleAngle: ta, titleOrient, } = getHeaderProperties(['titleAnchor', 'titleAngle', 'titleOrient'], facetFieldDef.header, config, channel);
    const headerChannel = getHeaderChannel(channel, titleOrient);
    const titleAngle = normalizeAngle(ta);
    return {
        name: `${channel}-title`,
        type: 'group',
        role: `${headerChannel}-title`,
        title: {
            text: title,
            ...(channel === 'row' ? { orient: 'left' } : {}),
            style: 'guide-title',
            ...defaultHeaderGuideBaseline(titleAngle, headerChannel),
            ...defaultHeaderGuideAlign(headerChannel, titleAngle, titleAnchor),
            ...assembleHeaderProperties(config, facetFieldDef, channel, HEADER_TITLE_PROPERTIES, HEADER_TITLE_PROPERTIES_MAP),
        },
    };
}
function defaultHeaderGuideAlign(headerChannel, angle, anchor = 'middle') {
    switch (anchor) {
        case 'start':
            return { align: 'left' };
        case 'end':
            return { align: 'right' };
    }
    const align = defaultLabelAlign(angle, headerChannel === 'row' ? 'left' : 'top', headerChannel === 'row' ? 'y' : 'x');
    return align ? { align } : {};
}
function defaultHeaderGuideBaseline(angle, channel) {
    const baseline = defaultLabelBaseline(angle, channel === 'row' ? 'left' : 'top', channel === 'row' ? 'y' : 'x', true);
    return baseline ? { baseline } : {};
}
function assembleHeaderGroups(model, channel) {
    const layoutHeader = model.component.layoutHeaders[channel];
    const groups = [];
    for (const headerType of HEADER_TYPES) {
        if (layoutHeader[headerType]) {
            for (const headerComponent of layoutHeader[headerType]) {
                const group = assembleHeaderGroup(model, channel, headerType, layoutHeader, headerComponent);
                if (group != null) {
                    groups.push(group);
                }
            }
        }
    }
    return groups;
}
function getSort$1(facetFieldDef, channel) {
    const { sort } = facetFieldDef;
    if (isSortField(sort)) {
        return {
            field: vgField(sort, { expr: 'datum' }),
            order: sort.order ?? 'ascending',
        };
    }
    else if (isArray(sort)) {
        return {
            field: sortArrayIndexField(facetFieldDef, channel, { expr: 'datum' }),
            order: 'ascending',
        };
    }
    else {
        return {
            field: vgField(facetFieldDef, { expr: 'datum' }),
            order: sort ?? 'ascending',
        };
    }
}
function assembleLabelTitle(facetFieldDef, channel, config) {
    const { format, formatType, labelAngle, labelAnchor, labelOrient, labelExpr } = getHeaderProperties(['format', 'formatType', 'labelAngle', 'labelAnchor', 'labelOrient', 'labelExpr'], facetFieldDef.header, config, channel);
    const titleTextExpr = formatSignalRef({
        fieldOrDatumDef: facetFieldDef,
        format,
        formatType,
        expr: 'parent',
        config,
    }).signal;
    const headerChannel = getHeaderChannel(channel, labelOrient);
    return {
        text: {
            signal: labelExpr
                ? replaceAll(replaceAll(labelExpr, 'datum.label', titleTextExpr), 'datum.value', vgField(facetFieldDef, { expr: 'parent' }))
                : titleTextExpr,
        },
        ...(channel === 'row' ? { orient: 'left' } : {}),
        style: 'guide-label',
        frame: 'group',
        ...defaultHeaderGuideBaseline(labelAngle, headerChannel),
        ...defaultHeaderGuideAlign(headerChannel, labelAngle, labelAnchor),
        ...assembleHeaderProperties(config, facetFieldDef, channel, HEADER_LABEL_PROPERTIES, HEADER_LABEL_PROPERTIES_MAP),
    };
}
function assembleHeaderGroup(model, channel, headerType, layoutHeader, headerComponent) {
    if (headerComponent) {
        let title = null;
        const { facetFieldDef } = layoutHeader;
        const config = model.config ? model.config : undefined;
        if (facetFieldDef && headerComponent.labels) {
            const { labelOrient } = getHeaderProperties(['labelOrient'], facetFieldDef.header, config, channel);
            // Include label title in the header if orient aligns with the channel
            if ((channel === 'row' && !contains(['top', 'bottom'], labelOrient)) ||
                (channel === 'column' && !contains(['left', 'right'], labelOrient))) {
                title = assembleLabelTitle(facetFieldDef, channel, config);
            }
        }
        const isFacetWithoutRowCol = isFacetModel(model) && !isFacetMapping(model.facet);
        const axes = headerComponent.axes;
        const hasAxes = axes?.length > 0;
        if (title || hasAxes) {
            const sizeChannel = channel === 'row' ? 'height' : 'width';
            return {
                name: model.getName(`${channel}_${headerType}`),
                type: 'group',
                role: `${channel}-${headerType}`,
                ...(layoutHeader.facetFieldDef
                    ? {
                        from: { data: model.getName(`${channel}_domain`) },
                        sort: getSort$1(facetFieldDef, channel),
                    }
                    : {}),
                ...(hasAxes && isFacetWithoutRowCol
                    ? {
                        from: { data: model.getName(`facet_domain_${channel}`) },
                    }
                    : {}),
                ...(title ? { title } : {}),
                ...(headerComponent.sizeSignal
                    ? {
                        encode: {
                            update: {
                                [sizeChannel]: headerComponent.sizeSignal,
                            },
                        },
                    }
                    : {}),
                ...(hasAxes ? { axes } : {}),
            };
        }
    }
    return null;
}
const LAYOUT_TITLE_BAND = {
    column: {
        start: 0,
        end: 1,
    },
    row: {
        start: 1,
        end: 0,
    },
};
function getLayoutTitleBand(titleAnchor, headerChannel) {
    return LAYOUT_TITLE_BAND[headerChannel][titleAnchor];
}
function assembleLayoutTitleBand(headerComponentIndex, config) {
    const titleBand = {};
    for (const channel of FACET_CHANNELS) {
        const headerComponent = headerComponentIndex[channel];
        if (headerComponent?.facetFieldDef) {
            const { titleAnchor, titleOrient } = getHeaderProperties(['titleAnchor', 'titleOrient'], headerComponent.facetFieldDef.header, config, channel);
            const headerChannel = getHeaderChannel(channel, titleOrient);
            const band = getLayoutTitleBand(titleAnchor, headerChannel);
            if (band !== undefined) {
                titleBand[headerChannel] = band;
            }
        }
    }
    return isEmpty(titleBand) ? undefined : titleBand;
}
function assembleHeaderProperties(config, facetFieldDef, channel, properties, propertiesMap) {
    const props = {};
    for (const prop of properties) {
        if (!propertiesMap[prop]) {
            continue;
        }
        const value = getHeaderProperty(prop, facetFieldDef?.header, config, channel);
        if (value !== undefined) {
            props[propertiesMap[prop]] = value;
        }
    }
    return props;
}

function assembleLayoutSignals(model) {
    return [
        ...sizeSignals(model, 'width'),
        ...sizeSignals(model, 'height'),
        ...sizeSignals(model, 'childWidth'),
        ...sizeSignals(model, 'childHeight'),
    ];
}
function sizeSignals(model, sizeType) {
    const channel = sizeType === 'width' ? 'x' : 'y';
    const size = model.component.layoutSize.get(sizeType);
    if (size == null || size === 'merged') {
        return [];
    }
    // Read size signal name from name map, just in case it is the top-level size signal that got renamed.
    const name = model.getSizeSignalRef(sizeType).signal;
    if (size === 'step') {
        const scaleComponent = model.getScaleComponent(channel);
        if (scaleComponent) {
            const type = scaleComponent.get('type');
            const range = scaleComponent.get('range');
            if (hasDiscreteDomain(type) && isVgRangeStep(range)) {
                const scaleName = model.scaleName(channel);
                if (isFacetModel(model.parent)) {
                    // If parent is facet and this is an independent scale, return only signal signal
                    // as the width/height will be calculated using the cardinality from
                    // facet's aggregate rather than reading from scale domain
                    const parentResolve = model.parent.component.resolve;
                    if (parentResolve.scale[channel] === 'independent') {
                        return [stepSignal(scaleName, range)];
                    }
                }
                return [
                    stepSignal(scaleName, range),
                    {
                        name,
                        update: sizeExpr(scaleName, scaleComponent, `domain('${scaleName}').length`),
                    },
                ];
            }
        }
        /* istanbul ignore next: Condition should not happen -- only for warning in development. */
        throw new Error('layout size is step although width/height is not step.');
    }
    else if (size == 'container') {
        const isWidth = name.endsWith('width');
        const expr = isWidth ? 'containerSize()[0]' : 'containerSize()[1]';
        const defaultValue = getViewConfigContinuousSize(model.config.view, isWidth ? 'width' : 'height');
        const safeExpr = `isFinite(${expr}) ? ${expr} : ${defaultValue}`;
        return [{ name, init: safeExpr, on: [{ update: safeExpr, events: 'window:resize' }] }];
    }
    else {
        return [
            {
                name,
                value: size,
            },
        ];
    }
}
function stepSignal(scaleName, range) {
    const name = `${scaleName}_step`;
    if (isSignalRef(range.step)) {
        return { name, update: range.step.signal };
    }
    else {
        return { name, value: range.step };
    }
}
function sizeExpr(scaleName, scaleComponent, cardinality) {
    const type = scaleComponent.get('type');
    const padding = scaleComponent.get('padding');
    const paddingOuter = getFirstDefined(scaleComponent.get('paddingOuter'), padding);
    let paddingInner = scaleComponent.get('paddingInner');
    paddingInner =
        type === 'band'
            ? // only band has real paddingInner
                paddingInner !== undefined
                    ? paddingInner
                    : padding
            : // For point, as calculated in https://github.com/vega/vega-scale/blob/master/src/band.js#L128,
                // it's equivalent to have paddingInner = 1 since there is only n-1 steps between n points.
                1;
    return `bandspace(${cardinality}, ${signalOrStringValue(paddingInner)}, ${signalOrStringValue(paddingOuter)}) * ${scaleName}_step`;
}

function getSizeTypeFromLayoutSizeType(layoutSizeType) {
    return layoutSizeType === 'childWidth' ? 'width' : layoutSizeType === 'childHeight' ? 'height' : layoutSizeType;
}

function guideEncodeEntry(encoding, model) {
    return keys(encoding).reduce((encode, channel) => {
        return {
            ...encode,
            ...wrapCondition({
                model,
                channelDef: encoding[channel],
                vgChannel: channel,
                mainRefFn: (def) => signalOrValueRef(def.value),
                invalidValueRef: undefined, // guide encoding won't show invalid values for the scale
            }),
        };
    }, {});
}

function defaultScaleResolve(channel, model) {
    if (isFacetModel(model)) {
        return channel === 'theta' ? 'independent' : 'shared';
    }
    else if (isLayerModel(model)) {
        return 'shared';
    }
    else if (isConcatModel(model)) {
        return isXorY(channel) || channel === 'theta' || channel === 'radius' ? 'independent' : 'shared';
    }
    /* istanbul ignore next: should never reach here. */
    throw new Error('invalid model type for resolve');
}
function parseGuideResolve(resolve, channel) {
    const channelScaleResolve = resolve.scale[channel];
    const guide = isXorY(channel) ? 'axis' : 'legend';
    if (channelScaleResolve === 'independent') {
        if (resolve[guide][channel] === 'shared') {
            warn(independentScaleMeansIndependentGuide(channel));
        }
        return 'independent';
    }
    return resolve[guide][channel] || 'shared';
}

const LEGEND_COMPONENT_PROPERTY_INDEX = {
    ...COMMON_LEGEND_PROPERTY_INDEX,
    disable: 1,
    labelExpr: 1,
    selections: 1,
    // channel scales
    opacity: 1,
    shape: 1,
    stroke: 1,
    fill: 1,
    size: 1,
    strokeWidth: 1,
    strokeDash: 1,
    // encode
    encode: 1,
};
const LEGEND_COMPONENT_PROPERTIES = keys(LEGEND_COMPONENT_PROPERTY_INDEX);
class LegendComponent extends Split {
}

const legendEncodeRules = {
    symbols,
    gradient,
    labels: labels$1,
    entries,
};
function symbols(symbolsSpec, { fieldOrDatumDef, model, channel, legendCmpt, legendType }) {
    if (legendType !== 'symbol') {
        return undefined;
    }
    const { markDef, encoding, config, mark } = model;
    const filled = markDef.filled && mark !== 'trail';
    let out = {
        ...applyMarkConfig({}, model, FILL_STROKE_CONFIG),
        ...color(model, { filled }),
    }; // FIXME: remove this when VgEncodeEntry is compatible with SymbolEncodeEntry
    const symbolOpacity = legendCmpt.get('symbolOpacity') ?? config.legend.symbolOpacity;
    const symbolFillColor = legendCmpt.get('symbolFillColor') ?? config.legend.symbolFillColor;
    const symbolStrokeColor = legendCmpt.get('symbolStrokeColor') ?? config.legend.symbolStrokeColor;
    const opacity = symbolOpacity === undefined ? (getMaxValue(encoding.opacity) ?? markDef.opacity) : undefined;
    if (out.fill) {
        // for fill legend, we don't want any fill in symbol
        if (channel === 'fill' || (filled && channel === COLOR)) {
            delete out.fill;
        }
        else if (hasProperty(out.fill, 'field')) {
            // For others, set fill to some opaque value (or nothing if a color is already set)
            if (symbolFillColor) {
                delete out.fill;
            }
            else {
                out.fill = signalOrValueRef(config.legend.symbolBaseFillColor ?? 'black');
                out.fillOpacity = signalOrValueRef(opacity ?? 1);
            }
        }
        else if (isArray(out.fill)) {
            const fill = getFirstConditionValue(encoding.fill ?? encoding.color) ?? markDef.fill ?? (filled && markDef.color);
            if (fill) {
                out.fill = signalOrValueRef(fill);
            }
        }
    }
    if (out.stroke) {
        if (channel === 'stroke' || (!filled && channel === COLOR)) {
            delete out.stroke;
        }
        else if (hasProperty(out.stroke, 'field') || symbolStrokeColor) {
            // For others, remove stroke field
            delete out.stroke;
        }
        else if (isArray(out.stroke)) {
            const stroke = getFirstDefined(getFirstConditionValue(encoding.stroke || encoding.color), markDef.stroke, filled ? markDef.color : undefined);
            if (stroke) {
                out.stroke = { value: stroke };
            }
        }
    }
    if (channel !== OPACITY) {
        const condition = isFieldDef(fieldOrDatumDef) && selectedCondition(model, legendCmpt, fieldOrDatumDef);
        if (condition) {
            out.opacity = [
                { test: condition, ...signalOrValueRef(opacity ?? 1) },
                signalOrValueRef(config.legend.unselectedOpacity),
            ];
        }
        else if (opacity) {
            out.opacity = signalOrValueRef(opacity);
        }
    }
    out = { ...out, ...symbolsSpec };
    return isEmpty(out) ? undefined : out;
}
function gradient(gradientSpec, { model, legendType, legendCmpt }) {
    if (legendType !== 'gradient') {
        return undefined;
    }
    const { config, markDef, encoding } = model;
    let out = {};
    const gradientOpacity = legendCmpt.get('gradientOpacity') ?? config.legend.gradientOpacity;
    const opacity = gradientOpacity === undefined ? getMaxValue(encoding.opacity) || markDef.opacity : undefined;
    if (opacity) {
        // only apply opacity if it is neither zero or undefined
        out.opacity = signalOrValueRef(opacity);
    }
    out = { ...out, ...gradientSpec };
    return isEmpty(out) ? undefined : out;
}
function labels$1(specifiedlabelsSpec, { fieldOrDatumDef, model, channel, legendCmpt }) {
    const legend = model.legend(channel) || {};
    const config = model.config;
    const condition = isFieldDef(fieldOrDatumDef) ? selectedCondition(model, legendCmpt, fieldOrDatumDef) : undefined;
    const opacity = condition ? [{ test: condition, value: 1 }, { value: config.legend.unselectedOpacity }] : undefined;
    const { format, formatType } = legend;
    let text = undefined;
    if (isCustomFormatType(formatType)) {
        text = formatCustomType({
            fieldOrDatumDef,
            field: 'datum.value',
            format,
            formatType,
            config,
        });
    }
    else if (format === undefined && formatType === undefined && config.customFormatTypes) {
        if (fieldOrDatumDef.type === 'quantitative' && config.numberFormatType) {
            text = formatCustomType({
                fieldOrDatumDef,
                field: 'datum.value',
                format: config.numberFormat,
                formatType: config.numberFormatType,
                config,
            });
        }
        else if (fieldOrDatumDef.type === 'temporal' &&
            config.timeFormatType &&
            isFieldDef(fieldOrDatumDef) &&
            fieldOrDatumDef.timeUnit === undefined) {
            text = formatCustomType({
                fieldOrDatumDef,
                field: 'datum.value',
                format: config.timeFormat,
                formatType: config.timeFormatType,
                config,
            });
        }
    }
    const labelsSpec = {
        ...(opacity ? { opacity } : {}),
        ...(text ? { text } : {}),
        ...specifiedlabelsSpec,
    };
    return isEmpty(labelsSpec) ? undefined : labelsSpec;
}
function entries(entriesSpec, { legendCmpt }) {
    const selections = legendCmpt.get('selections');
    return selections?.length ? { ...entriesSpec, fill: { value: 'transparent' } } : entriesSpec;
}
function getMaxValue(channelDef) {
    return getConditionValue(channelDef, (v, conditionalDef) => Math.max(v, conditionalDef.value));
}
function getFirstConditionValue(channelDef) {
    return getConditionValue(channelDef, (v, conditionalDef) => {
        return getFirstDefined(v, conditionalDef.value);
    });
}
function getConditionValue(channelDef, reducer) {
    if (hasConditionalValueDef(channelDef)) {
        return array$2(channelDef.condition).reduce(reducer, channelDef.value);
    }
    else if (isValueDef(channelDef)) {
        return channelDef.value;
    }
    return undefined;
}
function selectedCondition(model, legendCmpt, fieldDef) {
    const selections = legendCmpt.get('selections');
    if (!selections?.length)
        return undefined;
    const field = $(fieldDef.field);
    return selections
        .map((name) => {
        const store = $(varName(name) + STORE);
        return `(!length(data(${store})) || (${name}[${field}] && indexof(${name}[${field}], datum.value) >= 0))`;
    })
        .join(' || ');
}

const legendRules = {
    direction: ({ direction }) => direction,
    format: ({ fieldOrDatumDef, legend, config }) => {
        const { format, formatType } = legend;
        return guideFormat(fieldOrDatumDef, fieldOrDatumDef.type, format, formatType, config, false);
    },
    formatType: ({ legend, fieldOrDatumDef, scaleType }) => {
        const { formatType } = legend;
        return guideFormatType(formatType, fieldOrDatumDef, scaleType);
    },
    gradientLength: (params) => {
        const { legend, legendConfig } = params;
        return legend.gradientLength ?? legendConfig.gradientLength ?? defaultGradientLength(params);
    },
    labelOverlap: ({ legend, legendConfig, scaleType }) => legend.labelOverlap ?? legendConfig.labelOverlap ?? defaultLabelOverlap(scaleType),
    symbolType: ({ legend, markDef, channel, encoding }) => legend.symbolType ?? defaultSymbolType(markDef.type, channel, encoding.shape, markDef.shape),
    title: ({ fieldOrDatumDef, config }) => title(fieldOrDatumDef, config, { allowDisabling: true }),
    type: ({ legendType, scaleType, channel }) => {
        if (isColorChannel(channel) && isContinuousToContinuous(scaleType)) {
            if (legendType === 'gradient') {
                return undefined;
            }
        }
        else if (legendType === 'symbol') {
            return undefined;
        }
        return legendType;
    }, // depended by other property, let's define upfront
    values: ({ fieldOrDatumDef, legend }) => values(legend, fieldOrDatumDef),
};
function values(legend, fieldOrDatumDef) {
    const vals = legend.values;
    if (isArray(vals)) {
        return valueArray(fieldOrDatumDef, vals);
    }
    else if (isSignalRef(vals)) {
        return vals;
    }
    return undefined;
}
function defaultSymbolType(mark, channel, shapeChannelDef, markShape) {
    if (channel !== 'shape') {
        // use the value from the shape encoding or the mark config if they exist
        const shape = getFirstConditionValue(shapeChannelDef) ?? markShape;
        if (shape) {
            return shape;
        }
    }
    switch (mark) {
        case 'bar':
        case 'rect':
        case 'image':
        case 'square':
            return 'square';
        case 'line':
        case 'trail':
        case 'rule':
            return 'stroke';
        case 'arc':
        case 'point':
        case 'circle':
        case 'tick':
        case 'geoshape':
        case 'area':
        case 'text':
            return 'circle';
    }
}
function getLegendType(params) {
    const { legend } = params;
    return getFirstDefined(legend.type, defaultType$1(params));
}
function defaultType$1({ channel, timeUnit, scaleType, }) {
    // Following the logic in https://github.com/vega/vega-parser/blob/master/src/parsers/legend.js
    if (isColorChannel(channel)) {
        if (contains(['quarter', 'month', 'day'], timeUnit)) {
            return 'symbol';
        }
        if (isContinuousToContinuous(scaleType)) {
            return 'gradient';
        }
    }
    return 'symbol';
}
function getDirection({ legendConfig, legendType, orient, legend, }) {
    return (legend.direction ??
        legendConfig[legendType ? 'gradientDirection' : 'symbolDirection'] ??
        defaultDirection(orient, legendType));
}
function defaultDirection(orient, legendType) {
    switch (orient) {
        case 'top':
        case 'bottom':
            return 'horizontal';
        case 'left':
        case 'right':
        case 'none':
        case undefined: // undefined = "right" in Vega
            return undefined; // vertical is Vega's default
        default:
            // top-left / ...
            // For inner legend, uses compact layout like Tableau
            return legendType === 'gradient' ? 'horizontal' : undefined;
    }
}
function defaultGradientLength({ legendConfig, model, direction, orient, scaleType, }) {
    const { gradientHorizontalMaxLength, gradientHorizontalMinLength, gradientVerticalMaxLength, gradientVerticalMinLength, } = legendConfig;
    if (isContinuousToContinuous(scaleType)) {
        if (direction === 'horizontal') {
            if (orient === 'top' || orient === 'bottom') {
                return gradientLengthSignal(model, 'width', gradientHorizontalMinLength, gradientHorizontalMaxLength);
            }
            else {
                return gradientHorizontalMinLength;
            }
        }
        else {
            // vertical / undefined (Vega uses vertical by default)
            return gradientLengthSignal(model, 'height', gradientVerticalMinLength, gradientVerticalMaxLength);
        }
    }
    return undefined;
}
function gradientLengthSignal(model, sizeType, min, max) {
    const sizeSignal = model.getSizeSignalRef(sizeType).signal;
    return { signal: `clamp(${sizeSignal}, ${min}, ${max})` };
}
function defaultLabelOverlap(scaleType) {
    if (contains(['quantile', 'threshold', 'log', 'symlog'], scaleType)) {
        return 'greedy';
    }
    return undefined;
}

function parseLegend(model) {
    const legendComponent = isUnitModel(model) ? parseUnitLegend(model) : parseNonUnitLegend(model);
    model.component.legends = legendComponent;
    return legendComponent;
}
function parseUnitLegend(model) {
    const { encoding } = model;
    const legendComponent = {};
    for (const channel of [COLOR, ...LEGEND_SCALE_CHANNELS]) {
        const def = getFieldOrDatumDef(encoding[channel]);
        if (!def || !model.getScaleComponent(channel)) {
            continue;
        }
        if (channel === SHAPE && isFieldDef(def) && def.type === GEOJSON) {
            continue;
        }
        legendComponent[channel] = parseLegendForChannel(model, channel);
    }
    return legendComponent;
}
function getLegendDefWithScale(model, channel) {
    const scale = model.scaleName(channel);
    if (model.mark === 'trail') {
        if (channel === 'color') {
            // trail is a filled mark, but its default symbolType ("stroke") should use "stroke"
            return { stroke: scale };
        }
        else if (channel === 'size') {
            return { strokeWidth: scale };
        }
    }
    if (channel === 'color') {
        return model.markDef.filled ? { fill: scale } : { stroke: scale };
    }
    return { [channel]: scale };
}
function isExplicit$1(value, property, legend, fieldDef) {
    switch (property) {
        case 'disable':
            return legend !== undefined; // if axis is specified or null/false, then its enable/disable state is explicit
        case 'values':
            // specified legend.values is already respected, but may get transformed.
            return !!legend?.values;
        case 'title':
            // title can be explicit if fieldDef.title is set
            if (property === 'title' && value === fieldDef?.title) {
                return true;
            }
    }
    // Otherwise, things are explicit if the returned value matches the specified property
    return value === (legend || {})[property];
}
function parseLegendForChannel(model, channel) {
    let legend = model.legend(channel);
    const { markDef, encoding, config } = model;
    const legendConfig = config.legend;
    const legendCmpt = new LegendComponent({}, getLegendDefWithScale(model, channel));
    parseInteractiveLegend(model, channel, legendCmpt);
    const disable = legend !== undefined ? !legend : legendConfig.disable;
    legendCmpt.set('disable', disable, legend !== undefined);
    if (disable) {
        return legendCmpt;
    }
    legend = legend || {};
    const scaleType = model.getScaleComponent(channel).get('type');
    const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]);
    const timeUnit = isFieldDef(fieldOrDatumDef) ? normalizeTimeUnit(fieldOrDatumDef.timeUnit)?.unit : undefined;
    const orient = legend.orient || config.legend.orient || 'right';
    const legendType = getLegendType({ legend, channel, timeUnit, scaleType });
    const direction = getDirection({ legend, legendType, orient, legendConfig });
    const ruleParams = {
        legend,
        channel,
        model,
        markDef,
        encoding,
        fieldOrDatumDef,
        legendConfig,
        config,
        scaleType,
        orient,
        legendType,
        direction,
    };
    for (const property of LEGEND_COMPONENT_PROPERTIES) {
        if ((legendType === 'gradient' && property.startsWith('symbol')) ||
            (legendType === 'symbol' && property.startsWith('gradient'))) {
            continue;
        }
        const value = property in legendRules ? legendRules[property](ruleParams) : legend[property];
        if (value !== undefined) {
            const explicit = isExplicit$1(value, property, legend, model.fieldDef(channel));
            if (explicit || config.legend[property] === undefined) {
                legendCmpt.set(property, value, explicit);
            }
        }
    }
    const legendEncoding = legend?.encoding ?? {};
    const selections = legendCmpt.get('selections');
    const legendEncode = {};
    const legendEncodeParams = { fieldOrDatumDef, model, channel, legendCmpt, legendType };
    for (const part of ['labels', 'legend', 'title', 'symbols', 'gradient', 'entries']) {
        // FIXME: remove as any (figure out what legendEncoding.entries is)
        const legendEncodingPart = guideEncodeEntry(legendEncoding[part] ?? {}, model);
        const value = part in legendEncodeRules
            ? legendEncodeRules[part](legendEncodingPart, legendEncodeParams) // apply rule
            : legendEncodingPart; // no rule -- just default values
        if (value !== undefined && !isEmpty(value)) {
            legendEncode[part] = {
                ...(selections?.length && isFieldDef(fieldOrDatumDef)
                    ? { name: `${varName(fieldOrDatumDef.field)}_legend_${part}` }
                    : {}),
                ...(selections?.length ? { interactive: true } : {}),
                update: selections?.length ? { ...value, cursor: { value: 'pointer' } } : value,
            };
        }
    }
    if (!isEmpty(legendEncode)) {
        legendCmpt.set('encode', legendEncode, !!legend?.encoding);
    }
    return legendCmpt;
}
function parseNonUnitLegend(model) {
    const { legends, resolve } = model.component;
    for (const child of model.children) {
        parseLegend(child);
        for (const channel of keys(child.component.legends)) {
            resolve.legend[channel] = parseGuideResolve(model.component.resolve, channel);
            if (resolve.legend[channel] === 'shared') {
                // If the resolve says shared (and has not been overridden)
                // We will try to merge and see if there is a conflict
                legends[channel] = mergeLegendComponent(legends[channel], child.component.legends[channel]);
                if (!legends[channel]) {
                    // If merge returns nothing, there is a conflict so we cannot make the legend shared.
                    // Thus, mark legend as independent and remove the legend component.
                    resolve.legend[channel] = 'independent';
                    delete legends[channel];
                }
            }
        }
    }
    for (const channel of keys(legends)) {
        for (const child of model.children) {
            if (!child.component.legends[channel]) {
                // skip if the child does not have a particular legend
                continue;
            }
            if (resolve.legend[channel] === 'shared') {
                // After merging shared legend, make sure to remove legend from child
                delete child.component.legends[channel];
            }
        }
    }
    return legends;
}
function mergeLegendComponent(mergedLegend, childLegend) {
    if (!mergedLegend) {
        return childLegend.clone();
    }
    const mergedOrient = mergedLegend.getWithExplicit('orient');
    const childOrient = childLegend.getWithExplicit('orient');
    if (mergedOrient.explicit && childOrient.explicit && mergedOrient.value !== childOrient.value) {
        // TODO: throw warning if resolve is explicit (We don't have info about explicit/implicit resolve yet.)
        // Cannot merge due to inconsistent orient
        return undefined;
    }
    let typeMerged = false;
    // Otherwise, let's merge
    for (const prop of LEGEND_COMPONENT_PROPERTIES) {
        const mergedValueWithExplicit = mergeValuesWithExplicit(mergedLegend.getWithExplicit(prop), childLegend.getWithExplicit(prop), prop, 'legend', 
        // Tie breaker function
        (v1, v2) => {
            switch (prop) {
                case 'symbolType':
                    return mergeSymbolType(v1, v2);
                case 'title':
                    return mergeTitleComponent(v1, v2);
                case 'type':
                    // There are only two types. If we have different types, then prefer symbol over gradient.
                    typeMerged = true;
                    return makeImplicit('symbol');
            }
            return defaultTieBreaker(v1, v2, prop, 'legend');
        });
        mergedLegend.setWithExplicit(prop, mergedValueWithExplicit);
    }
    if (typeMerged) {
        if (mergedLegend.implicit?.encode?.gradient) {
            deleteNestedProperty(mergedLegend.implicit, ['encode', 'gradient']);
        }
        if (mergedLegend.explicit?.encode?.gradient) {
            deleteNestedProperty(mergedLegend.explicit, ['encode', 'gradient']);
        }
    }
    return mergedLegend;
}
function mergeSymbolType(st1, st2) {
    if (st2.value === 'circle') {
        // prefer "circle" over "stroke"
        return st2;
    }
    return st1;
}

function rangeFormula(model, fieldDef, channel, config) {
    if (binRequiresRange(fieldDef, channel)) {
        // read format from axis or legend, if there is no format then use config.numberFormat
        const guide = isUnitModel(model)
            ? (model.axis(channel) ?? model.legend(channel) ?? {})
            : {};
        const startField = vgField(fieldDef, { expr: 'datum' });
        const endField = vgField(fieldDef, { expr: 'datum', binSuffix: 'end' });
        return {
            formulaAs: vgField(fieldDef, { binSuffix: 'range', forAs: true }),
            formula: binFormatExpression(startField, endField, guide.format, guide.formatType, config),
        };
    }
    return {};
}
function binKey(bin, field) {
    return `${binToString(bin)}_${field}`;
}
function getSignalsFromModel(model, key) {
    return {
        signal: model.getName(`${key}_bins`),
        extentSignal: model.getName(`${key}_extent`),
    };
}
function getBinSignalName(model, field, bin) {
    const normalizedBin = normalizeBin(bin, undefined) ?? {};
    const key = binKey(normalizedBin, field);
    return model.getName(`${key}_bins`);
}
function isBinTransform(t) {
    return 'as' in t;
}
function createBinComponent(t, bin, model) {
    let as;
    let span;
    if (isBinTransform(t)) {
        as = isString(t.as) ? [t.as, `${t.as}_end`] : [t.as[0], t.as[1]];
    }
    else {
        as = [vgField(t, { forAs: true }), vgField(t, { binSuffix: 'end', forAs: true })];
    }
    const normalizedBin = { ...normalizeBin(bin, undefined) };
    const key = binKey(normalizedBin, t.field);
    const { signal, extentSignal } = getSignalsFromModel(model, key);
    if (isParameterExtent(normalizedBin.extent)) {
        const ext = normalizedBin.extent;
        span = parseSelectionExtent(model, ext.param, ext);
        delete normalizedBin.extent; // Vega-Lite selection extent map to Vega's span property.
    }
    const binComponent = {
        bin: normalizedBin,
        field: t.field,
        as: [as],
        ...(signal ? { signal } : {}),
        ...(extentSignal ? { extentSignal } : {}),
        ...(span ? { span } : {}),
    };
    return { key, binComponent };
}
class BinNode extends DataFlowNode {
    bins;
    clone() {
        return new BinNode(null, duplicate(this.bins));
    }
    constructor(parent, bins) {
        super(parent);
        this.bins = bins;
    }
    static makeFromEncoding(parent, model) {
        const bins = model.reduceFieldDef((binComponentIndex, fieldDef, channel) => {
            if (isTypedFieldDef(fieldDef) && isBinning(fieldDef.bin)) {
                const { key, binComponent } = createBinComponent(fieldDef, fieldDef.bin, model);
                binComponentIndex[key] = {
                    ...binComponent,
                    ...binComponentIndex[key],
                    ...rangeFormula(model, fieldDef, channel, model.config),
                };
            }
            return binComponentIndex;
        }, {});
        if (isEmpty(bins)) {
            return null;
        }
        return new BinNode(parent, bins);
    }
    /**
     * Creates a bin node from BinTransform.
     * The optional parameter should provide
     */
    static makeFromTransform(parent, t, model) {
        const { key, binComponent } = createBinComponent(t, t.bin, model);
        return new BinNode(parent, {
            [key]: binComponent,
        });
    }
    /**
     * Merge bin nodes. This method either integrates the bin config from the other node
     * or if this node already has a bin config, renames the corresponding signal in the model.
     */
    merge(other, renameSignal) {
        for (const key of keys(other.bins)) {
            if (key in this.bins) {
                renameSignal(other.bins[key].signal, this.bins[key].signal);
                // Ensure that we don't have duplicate names for signal pairs
                this.bins[key].as = unique([...this.bins[key].as, ...other.bins[key].as], hash);
            }
            else {
                this.bins[key] = other.bins[key];
            }
        }
        for (const child of other.children) {
            other.removeChild(child);
            child.parent = this;
        }
        other.remove();
    }
    producedFields() {
        return new Set(vals(this.bins)
            .map((c) => c.as)
            .flat(2));
    }
    dependentFields() {
        return new Set(vals(this.bins).map((c) => c.field));
    }
    hash() {
        return `Bin ${hash(this.bins)}`;
    }
    assemble() {
        return vals(this.bins).flatMap((bin) => {
            const transform = [];
            const [binAs, ...remainingAs] = bin.as;
            const { extent, ...params } = bin.bin;
            const binTrans = {
                type: 'bin',
                field: replacePathInField(bin.field),
                as: binAs,
                signal: bin.signal,
                ...(!isParameterExtent(extent) ? { extent } : { extent: null }),
                ...(bin.span ? { span: { signal: `span(${bin.span})` } } : {}),
                ...params,
            };
            if (!extent && bin.extentSignal) {
                transform.push({
                    type: 'extent',
                    field: replacePathInField(bin.field),
                    signal: bin.extentSignal,
                });
                binTrans.extent = { signal: bin.extentSignal };
            }
            transform.push(binTrans);
            for (const as of remainingAs) {
                for (let i = 0; i < 2; i++) {
                    transform.push({
                        type: 'formula',
                        expr: vgField({ field: binAs[i] }, { expr: 'datum' }),
                        as: as[i],
                    });
                }
            }
            if (bin.formula) {
                transform.push({
                    type: 'formula',
                    expr: bin.formula,
                    as: bin.formulaAs,
                });
            }
            return transform;
        });
    }
}

function addDimension(dims, channel, fieldDef, model) {
    const channelDef2 = isUnitModel(model) ? model.encoding[getSecondaryRangeChannel(channel)] : undefined;
    if (isTypedFieldDef(fieldDef) &&
        isUnitModel(model) &&
        hasBandEnd(fieldDef, channelDef2, model.markDef, model.config)) {
        dims.add(vgField(fieldDef, {}));
        dims.add(vgField(fieldDef, { suffix: 'end' }));
        const { mark, markDef, config } = model;
        const bandPosition = getBandPosition({ fieldDef, markDef, config });
        if (isRectBasedMark(mark) && bandPosition !== 0.5 && isXorY(channel)) {
            dims.add(vgField(fieldDef, { suffix: OFFSETTED_RECT_START_SUFFIX }));
            dims.add(vgField(fieldDef, { suffix: OFFSETTED_RECT_END_SUFFIX }));
        }
        if (fieldDef.bin && binRequiresRange(fieldDef, channel)) {
            dims.add(vgField(fieldDef, { binSuffix: 'range' }));
        }
    }
    else if (isGeoPositionChannel(channel)) {
        const posChannel = getPositionChannelFromLatLong(channel);
        dims.add(model.getName(posChannel));
    }
    else {
        dims.add(vgField(fieldDef));
    }
    if (isScaleFieldDef(fieldDef) && isFieldRange(fieldDef.scale?.range)) {
        dims.add(fieldDef.scale.range.field);
    }
    return dims;
}
function mergeMeasures(parentMeasures, childMeasures) {
    for (const field of keys(childMeasures)) {
        // when we merge a measure, we either have to add an aggregation operator or even a new field
        const ops = childMeasures[field];
        for (const op of keys(ops)) {
            if (field in parentMeasures) {
                // add operator to existing measure field
                parentMeasures[field][op] = new Set([...(parentMeasures[field][op] ?? []), ...ops[op]]);
            }
            else {
                parentMeasures[field] = { [op]: ops[op] };
            }
        }
    }
}
class AggregateNode extends DataFlowNode {
    dimensions;
    measures;
    clone() {
        return new AggregateNode(null, new Set(this.dimensions), duplicate(this.measures));
    }
    /**
     * @param dimensions string set for dimensions
     * @param measures dictionary mapping field name => dict of aggregation functions and names to use
     */
    constructor(parent, dimensions, measures) {
        super(parent);
        this.dimensions = dimensions;
        this.measures = measures;
    }
    get groupBy() {
        return this.dimensions;
    }
    static makeFromEncoding(parent, model) {
        let isAggregate = false;
        model.forEachFieldDef((fd) => {
            if (fd.aggregate) {
                isAggregate = true;
            }
        });
        const meas = {};
        const dims = new Set();
        if (!isAggregate) {
            // no need to create this node if the model has no aggregation
            return null;
        }
        model.forEachFieldDef((fieldDef, channel) => {
            const { aggregate, field } = fieldDef;
            if (aggregate) {
                if (aggregate === 'count') {
                    meas['*'] ??= {};
                    meas['*']['count'] = new Set([vgField(fieldDef, { forAs: true })]);
                }
                else {
                    if (isArgminDef(aggregate) || isArgmaxDef(aggregate)) {
                        const op = isArgminDef(aggregate) ? 'argmin' : 'argmax';
                        const argField = aggregate[op];
                        meas[argField] ??= {};
                        meas[argField][op] = new Set([vgField({ op, field: argField }, { forAs: true })]);
                    }
                    else {
                        meas[field] ??= {};
                        meas[field][aggregate] = new Set([vgField(fieldDef, { forAs: true })]);
                    }
                    // For scale channel with domain === 'unaggregated', add min/max so we can use their union as unaggregated domain
                    if (isScaleChannel(channel) && model.scaleDomain(channel) === 'unaggregated') {
                        meas[field] ??= {};
                        meas[field]['min'] = new Set([vgField({ field, aggregate: 'min' }, { forAs: true })]);
                        meas[field]['max'] = new Set([vgField({ field, aggregate: 'max' }, { forAs: true })]);
                    }
                }
            }
            else {
                addDimension(dims, channel, fieldDef, model);
            }
        });
        if (dims.size + keys(meas).length === 0) {
            return null;
        }
        return new AggregateNode(parent, dims, meas);
    }
    static makeFromTransform(parent, t) {
        const dims = new Set();
        const meas = {};
        for (const s of t.aggregate) {
            const { op, field, as } = s;
            if (op) {
                if (op === 'count') {
                    meas['*'] ??= {};
                    meas['*']['count'] = new Set([as ? as : vgField(s, { forAs: true })]);
                }
                else {
                    meas[field] ??= {};
                    meas[field][op] ??= new Set();
                    meas[field][op].add(as ? as : vgField(s, { forAs: true }));
                }
            }
        }
        for (const s of t.groupby ?? []) {
            dims.add(s);
        }
        if (dims.size + keys(meas).length === 0) {
            return null;
        }
        return new AggregateNode(parent, dims, meas);
    }
    merge(other) {
        if (setEqual(this.dimensions, other.dimensions)) {
            mergeMeasures(this.measures, other.measures);
            return true;
        }
        debug('different dimensions, cannot merge');
        return false;
    }
    addDimensions(fields) {
        fields.forEach(this.dimensions.add, this.dimensions);
    }
    dependentFields() {
        return new Set([...this.dimensions, ...keys(this.measures)]);
    }
    producedFields() {
        const out = new Set();
        for (const field of keys(this.measures)) {
            for (const op of keys(this.measures[field])) {
                const m = this.measures[field][op];
                if (m.size === 0) {
                    out.add(`${op}_${field}`);
                }
                else {
                    m.forEach(out.add, out);
                }
            }
        }
        return out;
    }
    hash() {
        return `Aggregate ${hash({ dimensions: this.dimensions, measures: this.measures })}`;
    }
    assemble() {
        const ops = [];
        const fields = [];
        const as = [];
        for (const field of keys(this.measures)) {
            for (const op of keys(this.measures[field])) {
                for (const alias of this.measures[field][op]) {
                    as.push(alias);
                    ops.push(op);
                    fields.push(field === '*' ? null : replacePathInField(field));
                }
            }
        }
        const result = {
            type: 'aggregate',
            groupby: [...this.dimensions].map(replacePathInField),
            ops,
            fields,
            as,
        };
        return result;
    }
}

/**
 * A node that helps us track what fields we are faceting by.
 */
class FacetNode extends DataFlowNode {
    model;
    name;
    data;
    column;
    row;
    facet;
    childModel;
    /**
     * @param model The facet model.
     * @param name The name that this facet source will have.
     * @param data The source data for this facet data.
     */
    constructor(parent, model, name, data) {
        super(parent);
        this.model = model;
        this.name = name;
        this.data = data;
        for (const channel of FACET_CHANNELS) {
            const fieldDef = model.facet[channel];
            if (fieldDef) {
                const { bin, sort } = fieldDef;
                this[channel] = {
                    name: model.getName(`${channel}_domain`),
                    fields: [vgField(fieldDef), ...(isBinning(bin) ? [vgField(fieldDef, { binSuffix: 'end' })] : [])],
                    ...(isSortField(sort)
                        ? { sortField: sort }
                        : isArray(sort)
                            ? { sortIndexField: sortArrayIndexField(fieldDef, channel) }
                            : {}),
                };
            }
        }
        this.childModel = model.child;
    }
    hash() {
        let out = `Facet`;
        for (const channel of FACET_CHANNELS) {
            if (this[channel]) {
                out += ` ${channel.charAt(0)}:${hash(this[channel])}`;
            }
        }
        return out;
    }
    get fields() {
        const f = [];
        for (const channel of FACET_CHANNELS) {
            if (this[channel]?.fields) {
                f.push(...this[channel].fields);
            }
        }
        return f;
    }
    dependentFields() {
        const depFields = new Set(this.fields);
        for (const channel of FACET_CHANNELS) {
            if (this[channel]) {
                if (this[channel].sortField) {
                    depFields.add(this[channel].sortField.field);
                }
                if (this[channel].sortIndexField) {
                    depFields.add(this[channel].sortIndexField);
                }
            }
        }
        return depFields;
    }
    producedFields() {
        return new Set(); // facet does not produce any new fields
    }
    /**
     * The name to reference this source is its name.
     */
    getSource() {
        return this.name;
    }
    getChildIndependentFieldsWithStep() {
        const childIndependentFieldsWithStep = {};
        for (const channel of POSITION_SCALE_CHANNELS) {
            const childScaleComponent = this.childModel.component.scales[channel];
            if (childScaleComponent && !childScaleComponent.merged) {
                // independent scale
                const type = childScaleComponent.get('type');
                const range = childScaleComponent.get('range');
                if (hasDiscreteDomain(type) && isVgRangeStep(range)) {
                    const domain = assembleDomain(this.childModel, channel);
                    const field = getFieldFromDomain(domain);
                    if (field) {
                        childIndependentFieldsWithStep[channel] = field;
                    }
                    else {
                        warn(unknownField(channel));
                    }
                }
            }
        }
        return childIndependentFieldsWithStep;
    }
    assembleRowColumnHeaderData(channel, crossedDataName, childIndependentFieldsWithStep) {
        const childChannel = { row: 'y', column: 'x', facet: undefined }[channel];
        const fields = [];
        const ops = [];
        const as = [];
        if (childChannel && childIndependentFieldsWithStep && childIndependentFieldsWithStep[childChannel]) {
            if (crossedDataName) {
                // If there is a crossed data, calculate max
                fields.push(`distinct_${childIndependentFieldsWithStep[childChannel]}`);
                ops.push('max');
            }
            else {
                // If there is no crossed data, just calculate distinct
                fields.push(childIndependentFieldsWithStep[childChannel]);
                ops.push('distinct');
            }
            // Although it is technically a max, just name it distinct so it's easier to refer to it
            as.push(`distinct_${childIndependentFieldsWithStep[childChannel]}`);
        }
        const { sortField, sortIndexField } = this[channel];
        if (sortField) {
            const { op = DEFAULT_SORT_OP, field } = sortField;
            fields.push(field);
            ops.push(op);
            as.push(vgField(sortField, { forAs: true }));
        }
        else if (sortIndexField) {
            fields.push(sortIndexField);
            ops.push('max');
            as.push(sortIndexField);
        }
        return {
            name: this[channel].name,
            // Use data from the crossed one if it exist
            source: crossedDataName ?? this.data,
            transform: [
                {
                    type: 'aggregate',
                    groupby: this[channel].fields,
                    ...(fields.length
                        ? {
                            fields,
                            ops,
                            as,
                        }
                        : {}),
                },
            ],
        };
    }
    assembleFacetHeaderData(childIndependentFieldsWithStep) {
        const { columns } = this.model.layout;
        const { layoutHeaders } = this.model.component;
        const data = [];
        const hasSharedAxis = {};
        for (const headerChannel of HEADER_CHANNELS) {
            for (const headerType of HEADER_TYPES) {
                const headers = layoutHeaders[headerChannel]?.[headerType] ?? [];
                for (const header of headers) {
                    if (header.axes?.length > 0) {
                        hasSharedAxis[headerChannel] = true;
                        break;
                    }
                }
            }
            if (hasSharedAxis[headerChannel]) {
                const cardinality = `length(data("${this.facet.name}"))`;
                const stop = headerChannel === 'row'
                    ? columns
                        ? { signal: `ceil(${cardinality} / ${columns})` }
                        : 1
                    : columns
                        ? { signal: `min(${cardinality}, ${columns})` }
                        : { signal: cardinality };
                data.push({
                    name: `${this.facet.name}_${headerChannel}`,
                    transform: [
                        {
                            type: 'sequence',
                            start: 0,
                            stop,
                        },
                    ],
                });
            }
        }
        const { row, column } = hasSharedAxis;
        if (row || column) {
            data.unshift(this.assembleRowColumnHeaderData('facet', null, childIndependentFieldsWithStep));
        }
        return data;
    }
    assemble() {
        const data = [];
        let crossedDataName = null;
        const childIndependentFieldsWithStep = this.getChildIndependentFieldsWithStep();
        const { column, row, facet } = this;
        if (column && row && (childIndependentFieldsWithStep.x || childIndependentFieldsWithStep.y)) {
            // Need to create a cross dataset to correctly calculate cardinality
            crossedDataName = `cross_${this.column.name}_${this.row.name}`;
            const fields = [].concat(childIndependentFieldsWithStep.x ?? [], childIndependentFieldsWithStep.y ?? []);
            const ops = fields.map(() => 'distinct');
            data.push({
                name: crossedDataName,
                source: this.data,
                transform: [
                    {
                        type: 'aggregate',
                        groupby: this.fields,
                        fields,
                        ops,
                    },
                ],
            });
        }
        for (const channel of [COLUMN, ROW]) {
            if (this[channel]) {
                data.push(this.assembleRowColumnHeaderData(channel, crossedDataName, childIndependentFieldsWithStep));
            }
        }
        if (facet) {
            const facetData = this.assembleFacetHeaderData(childIndependentFieldsWithStep);
            if (facetData) {
                data.push(...facetData);
            }
        }
        return data;
    }
}

/**
 * Remove quotes from a string.
 */
function unquote(pattern) {
    if ((pattern.startsWith("'") && pattern.endsWith("'")) || (pattern.startsWith('"') && pattern.endsWith('"'))) {
        return pattern.slice(1, -1);
    }
    return pattern;
}
/**
 * @param field The field.
 * @param parse What to parse the field as.
 */
function parseExpression(field, parse) {
    const f = accessPathWithDatum(field);
    if (parse === 'number') {
        return `toNumber(${f})`;
    }
    else if (parse === 'boolean') {
        return `toBoolean(${f})`;
    }
    else if (parse === 'string') {
        return `toString(${f})`;
    }
    else if (parse === 'date') {
        return `toDate(${f})`;
    }
    else if (parse === 'flatten') {
        return f;
    }
    else if (parse.startsWith('date:')) {
        const specifier = unquote(parse.slice(5, parse.length));
        return `timeParse(${f},'${specifier}')`;
    }
    else if (parse.startsWith('utc:')) {
        const specifier = unquote(parse.slice(4, parse.length));
        return `utcParse(${f},'${specifier}')`;
    }
    else {
        warn(unrecognizedParse(parse));
        return null;
    }
}
function getImplicitFromFilterTransform(transform) {
    const implicit = {};
    forEachLeaf(transform.filter, (filter) => {
        if (isFieldPredicate(filter)) {
            // Automatically add a parse node for filters with filter objects
            let val = null;
            // For EqualFilter, just use the equal property.
            // For RangeFilter and OneOfFilter, all array members should have
            // the same type, so we only use the first one.
            if (isFieldEqualPredicate(filter)) {
                val = signalRefOrValue(filter.equal);
            }
            else if (isFieldLTEPredicate(filter)) {
                val = signalRefOrValue(filter.lte);
            }
            else if (isFieldLTPredicate(filter)) {
                val = signalRefOrValue(filter.lt);
            }
            else if (isFieldGTPredicate(filter)) {
                val = signalRefOrValue(filter.gt);
            }
            else if (isFieldGTEPredicate(filter)) {
                val = signalRefOrValue(filter.gte);
            }
            else if (isFieldRangePredicate(filter)) {
                // FIXME: remove as any
                val = filter.range[0];
            }
            else if (isFieldOneOfPredicate(filter)) {
                val = (filter.oneOf ?? filter.in)[0];
            } // else -- for filter expression, we can't infer anything
            if (val) {
                if (isDateTime(val)) {
                    implicit[filter.field] = 'date';
                }
                else if (isNumber$1(val)) {
                    implicit[filter.field] = 'number';
                }
                else if (isString(val)) {
                    implicit[filter.field] = 'string';
                }
            }
            if (filter.timeUnit) {
                implicit[filter.field] = 'date';
            }
        }
    });
    return implicit;
}
/**
 * Creates a parse node for implicit parsing from a model and updates ancestorParse.
 */
function getImplicitFromEncoding(model) {
    const implicit = {};
    function add(fieldDef) {
        if (isFieldOrDatumDefForTimeFormat(fieldDef)) {
            implicit[fieldDef.field] = 'date';
        }
        else if (fieldDef.type === 'quantitative' &&
            isMinMaxOp(fieldDef.aggregate) // we need to parse numbers to support correct min and max
        ) {
            implicit[fieldDef.field] = 'number';
        }
        else if (accessPathDepth(fieldDef.field) > 1) {
            // For non-date/non-number (strings and booleans), derive a flattened field for a referenced nested field.
            // (Parsing numbers / dates already flattens numeric and temporal fields.)
            if (!(fieldDef.field in implicit)) {
                implicit[fieldDef.field] = 'flatten';
            }
        }
        else if (isScaleFieldDef(fieldDef) && isSortField(fieldDef.sort) && accessPathDepth(fieldDef.sort.field) > 1) {
            // Flatten fields that we sort by but that are not otherwise flattened.
            if (!(fieldDef.sort.field in implicit)) {
                implicit[fieldDef.sort.field] = 'flatten';
            }
        }
    }
    if (isUnitModel(model) || isFacetModel(model)) {
        // Parse encoded fields
        model.forEachFieldDef((fieldDef, channel) => {
            if (isTypedFieldDef(fieldDef)) {
                add(fieldDef);
            }
            else {
                const mainChannel = getMainRangeChannel(channel);
                const mainFieldDef = model.fieldDef(mainChannel);
                add({
                    ...fieldDef,
                    type: mainFieldDef.type,
                });
            }
        });
    }
    // Parse quantitative dimension fields of path marks as numbers so that we sort them correctly.
    if (isUnitModel(model)) {
        const { mark, markDef, encoding } = model;
        if (isPathMark(mark) &&
            // No need to sort by dimension if we have a connected scatterplot (order channel is present)
            !model.encoding.order) {
            const dimensionChannel = markDef.orient === 'horizontal' ? 'y' : 'x';
            const dimensionChannelDef = encoding[dimensionChannel];
            if (isFieldDef(dimensionChannelDef) &&
                dimensionChannelDef.type === 'quantitative' &&
                !(dimensionChannelDef.field in implicit)) {
                implicit[dimensionChannelDef.field] = 'number';
            }
        }
    }
    return implicit;
}
/**
 * Creates a parse node for implicit parsing from a model and updates ancestorParse.
 */
function getImplicitFromSelection(model) {
    const implicit = {};
    if (isUnitModel(model) && model.component.selection) {
        for (const name of keys(model.component.selection)) {
            const selCmpt = model.component.selection[name];
            for (const proj of selCmpt.project.items) {
                if (!proj.channel && accessPathDepth(proj.field) > 1) {
                    implicit[proj.field] = 'flatten';
                }
            }
        }
    }
    return implicit;
}
class ParseNode extends DataFlowNode {
    _parse;
    clone() {
        return new ParseNode(null, duplicate(this._parse));
    }
    constructor(parent, parse) {
        super(parent);
        this._parse = parse;
    }
    hash() {
        return `Parse ${hash(this._parse)}`;
    }
    /**
     * Creates a parse node from a data.format.parse and updates ancestorParse.
     */
    static makeExplicit(parent, model, ancestorParse) {
        // Custom parse
        let explicit = {};
        const data = model.data;
        if (!isGenerator(data) && data?.format?.parse) {
            explicit = data.format.parse;
        }
        return this.makeWithAncestors(parent, explicit, {}, ancestorParse);
    }
    /**
     * Creates a parse node from "explicit" parse and "implicit" parse and updates ancestorParse.
     */
    static makeWithAncestors(parent, explicit, implicit, ancestorParse) {
        // We should not parse what has already been parsed in a parent (explicitly or implicitly) or what has been derived (maked as "derived"). We also don't need to flatten a field that has already been parsed.
        for (const field of keys(implicit)) {
            const parsedAs = ancestorParse.getWithExplicit(field);
            if (parsedAs.value !== undefined) {
                // We always ignore derived fields even if they are implicitly defined because we expect users to create the right types.
                if (parsedAs.explicit ||
                    parsedAs.value === implicit[field] ||
                    parsedAs.value === 'derived' ||
                    implicit[field] === 'flatten') {
                    delete implicit[field];
                }
                else {
                    warn(differentParse(field, implicit[field], parsedAs.value));
                }
            }
        }
        for (const field of keys(explicit)) {
            const parsedAs = ancestorParse.get(field);
            if (parsedAs !== undefined) {
                // Don't parse a field again if it has been parsed with the same type already.
                if (parsedAs === explicit[field]) {
                    delete explicit[field];
                }
                else {
                    warn(differentParse(field, explicit[field], parsedAs));
                }
            }
        }
        const parse = new Split(explicit, implicit);
        // add the format parse from this model so that children don't parse the same field again
        ancestorParse.copyAll(parse);
        // copy only non-null parses
        const p = {};
        for (const key of keys(parse.combine())) {
            const val = parse.get(key);
            if (val !== null) {
                p[key] = val;
            }
        }
        if (keys(p).length === 0 || ancestorParse.parseNothing) {
            return null;
        }
        return new ParseNode(parent, p);
    }
    get parse() {
        return this._parse;
    }
    merge(other) {
        this._parse = { ...this._parse, ...other.parse };
        other.remove();
    }
    /**
     * Assemble an object for Vega's format.parse property.
     */
    assembleFormatParse() {
        const formatParse = {};
        for (const field of keys(this._parse)) {
            const p = this._parse[field];
            if (accessPathDepth(field) === 1) {
                formatParse[field] = p;
            }
        }
        return formatParse;
    }
    // format parse depends and produces all fields in its parse
    producedFields() {
        return new Set(keys(this._parse));
    }
    dependentFields() {
        return new Set(keys(this._parse));
    }
    assembleTransforms(onlyNested = false) {
        return keys(this._parse)
            .filter((field) => (onlyNested ? accessPathDepth(field) > 1 : true))
            .map((field) => {
            const expr = parseExpression(field, this._parse[field]);
            if (!expr) {
                return null;
            }
            const formula = {
                type: 'formula',
                expr,
                as: removePathFromField(field), // Vega output is always flattened
            };
            return formula;
        })
            .filter((t) => t !== null);
    }
}

class IdentifierNode extends DataFlowNode {
    clone() {
        return new IdentifierNode(null);
    }
    constructor(parent) {
        super(parent);
    }
    dependentFields() {
        return new Set();
    }
    producedFields() {
        return new Set([SELECTION_ID]);
    }
    hash() {
        return 'Identifier';
    }
    assemble() {
        return { type: 'identifier', as: SELECTION_ID };
    }
}

class GraticuleNode extends DataFlowNode {
    params;
    clone() {
        return new GraticuleNode(null, this.params);
    }
    constructor(parent, params) {
        super(parent);
        this.params = params;
    }
    dependentFields() {
        return new Set();
    }
    producedFields() {
        return undefined; // there should never be a node before graticule
    }
    hash() {
        return `Graticule ${hash(this.params)}`;
    }
    assemble() {
        return {
            type: 'graticule',
            ...(this.params === true ? {} : this.params),
        };
    }
}

class SequenceNode extends DataFlowNode {
    params;
    clone() {
        return new SequenceNode(null, this.params);
    }
    constructor(parent, params) {
        super(parent);
        this.params = params;
    }
    dependentFields() {
        return new Set();
    }
    producedFields() {
        return new Set([this.params.as ?? 'data']);
    }
    hash() {
        return `Hash ${hash(this.params)}`;
    }
    assemble() {
        return {
            type: 'sequence',
            ...this.params,
        };
    }
}

class SourceNode extends DataFlowNode {
    _data;
    _name;
    _generator;
    constructor(data) {
        super(null); // source cannot have parent
        data ??= { name: 'source' };
        let format;
        if (!isGenerator(data)) {
            format = data.format ? { ...omit(data.format, ['parse']) } : {};
        }
        if (isInlineData(data)) {
            this._data = { values: data.values };
        }
        else if (isUrlData(data)) {
            this._data = { url: data.url };
            if (!format.type) {
                // Extract extension from URL using snippet from
                // http://stackoverflow.com/questions/680929/how-to-extract-extension-from-filename-string-in-javascript
                let defaultExtension = /(?:\.([^.]+))?$/.exec(data.url)[1];
                if (!contains(['json', 'csv', 'tsv', 'dsv', 'topojson'], defaultExtension)) {
                    defaultExtension = 'json';
                }
                // defaultExtension has type string but we ensure that it is DataFormatType above
                format.type = defaultExtension;
            }
        }
        else if (isSphereGenerator(data)) {
            // hardwire GeoJSON sphere data into output specification
            this._data = { values: [{ type: 'Sphere' }] };
        }
        else if (isNamedData(data) || isGenerator(data)) {
            this._data = {};
        }
        // set flag to check if generator
        this._generator = isGenerator(data);
        // any dataset can be named
        if (data.name) {
            this._name = data.name;
        }
        if (format && !isEmpty(format)) {
            this._data.format = format;
        }
    }
    dependentFields() {
        return new Set();
    }
    producedFields() {
        return undefined; // we don't know what this source produces
    }
    get data() {
        return this._data;
    }
    hasName() {
        return !!this._name;
    }
    get isGenerator() {
        return this._generator;
    }
    get dataName() {
        return this._name;
    }
    set dataName(name) {
        this._name = name;
    }
    set parent(parent) {
        throw new Error('Source nodes have to be roots.');
    }
    remove() {
        throw new Error('Source nodes are roots and cannot be removed.');
    }
    hash() {
        throw new Error('Cannot hash sources');
    }
    assemble() {
        return {
            name: this._name,
            ...this._data,
            transform: [],
        };
    }
}

/**
 * Whether this dataflow node is the source of the dataflow that produces data i.e. a source or a generator.
 */
function isDataSourceNode(node) {
    return node instanceof SourceNode || node instanceof GraticuleNode || node instanceof SequenceNode;
}
/**
 * Abstract base class for Dataflow optimizers.
 * Contains only mutation handling logic. Subclasses need to implement iteration logic.
 */
class Optimizer {
    #modified;
    constructor() {
        this.#modified = false;
    }
    // Once true, #modified is never set to false
    setModified() {
        this.#modified = true;
    }
    get modifiedFlag() {
        return this.#modified;
    }
}
/**
 * Starts from a node and runs the optimization function (the "run" method) upwards to the root,
 * depending on the continue and modified flag values returned by the optimization function.
 */
class BottomUpOptimizer extends Optimizer {
    /**
     * Compute a map of node depths that we can use to determine a topological sort order.
     */
    getNodeDepths(node, depth, depths) {
        depths.set(node, depth);
        for (const child of node.children) {
            this.getNodeDepths(child, depth + 1, depths);
        }
        return depths;
    }
    /**
     * Run the optimizer on all nodes starting from the leaves.
     */
    optimize(node) {
        const depths = this.getNodeDepths(node, 0, new Map());
        const topologicalSort = [...depths.entries()].sort((a, b) => b[1] - a[1]);
        for (const tuple of topologicalSort) {
            this.run(tuple[0]);
        }
        return this.modifiedFlag;
    }
}
/**
 * The optimizer function (the "run" method), is invoked on the given node and then continues recursively.
 */
class TopDownOptimizer extends Optimizer {
    /**
     * Run the optimizer depth first on all nodes starting from the roots.
     */
    optimize(node) {
        this.run(node);
        for (const child of node.children) {
            this.optimize(child);
        }
        return this.modifiedFlag;
    }
}

/**
 * Merge identical nodes at forks by comparing hashes.
 *
 * Does not need to iterate from leaves so we implement this with recursion as it's a bit simpler.
 */
class MergeIdenticalNodes extends TopDownOptimizer {
    mergeNodes(parent, nodes) {
        const mergedNode = nodes.shift();
        for (const node of nodes) {
            parent.removeChild(node);
            node.parent = mergedNode;
            node.remove();
        }
    }
    run(node) {
        const hashes = node.children.map((x) => x.hash());
        const buckets = {};
        for (let i = 0; i < hashes.length; i++) {
            if (buckets[hashes[i]] === undefined) {
                buckets[hashes[i]] = [node.children[i]];
            }
            else {
                buckets[hashes[i]].push(node.children[i]);
            }
        }
        for (const k of keys(buckets)) {
            if (buckets[k].length > 1) {
                this.setModified();
                this.mergeNodes(node, buckets[k]);
            }
        }
    }
}
/**
 * Optimizer that removes identifier nodes that are not needed for selections.
 */
class RemoveUnnecessaryIdentifierNodes extends TopDownOptimizer {
    requiresSelectionId;
    constructor(model) {
        super();
        this.requiresSelectionId = model && requiresSelectionId(model);
    }
    run(node) {
        if (node instanceof IdentifierNode) {
            // Only preserve IdentifierNodes if we have default discrete selections
            // in our model tree, and if the nodes come after tuple producing nodes.
            if (!(this.requiresSelectionId &&
                (isDataSourceNode(node.parent) || node.parent instanceof AggregateNode || node.parent instanceof ParseNode))) {
                this.setModified();
                node.remove();
            }
        }
    }
}
/**
 * Removes duplicate time unit nodes (as determined by the name of the output field) that may be generated due to
 * selections projected over time units. Only keeps the first time unit in any branch.
 *
 * This optimizer is a custom top down optimizer that keep track of produced fields in a branch.
 */
class RemoveDuplicateTimeUnits extends Optimizer {
    optimize(node) {
        this.run(node, new Set());
        return this.modifiedFlag;
    }
    run(node, timeUnitFields) {
        let producedFields = new Set();
        if (node instanceof TimeUnitNode) {
            producedFields = node.producedFields();
            if (hasIntersection(producedFields, timeUnitFields)) {
                this.setModified();
                node.removeFormulas(timeUnitFields);
                if (node.producedFields.length === 0) {
                    node.remove();
                }
            }
        }
        for (const child of node.children) {
            this.run(child, new Set([...timeUnitFields, ...producedFields]));
        }
    }
}
/**
 * Remove output nodes that are not required.
 */
class RemoveUnnecessaryOutputNodes extends TopDownOptimizer {
    constructor() {
        super();
    }
    run(node) {
        if (node instanceof OutputNode && !node.isRequired()) {
            this.setModified();
            node.remove();
        }
    }
}
/**
 * Move parse nodes up to forks and merges them if possible.
 */
class MoveParseUp extends BottomUpOptimizer {
    run(node) {
        if (isDataSourceNode(node)) {
            return;
        }
        if (node.numChildren() > 1) {
            // Don't move parse further up but continue with parent.
            return;
        }
        for (const child of node.children) {
            if (child instanceof ParseNode) {
                if (node instanceof ParseNode) {
                    this.setModified();
                    node.merge(child);
                }
                else {
                    // Don't swap with nodes that produce something that the parse node depends on (e.g. lookup).
                    if (fieldIntersection(node.producedFields(), child.dependentFields())) {
                        continue;
                    }
                    this.setModified();
                    child.swapWithParent();
                }
            }
        }
        return;
    }
}
/**
 * Inserts an intermediate ParseNode containing all non-conflicting parse fields and removes the empty ParseNodes.
 *
 * We assume that dependent paths that do not have a parse node can be just merged.
 */
class MergeParse extends BottomUpOptimizer {
    run(node) {
        const originalChildren = [...node.children];
        const parseChildren = node.children.filter((child) => child instanceof ParseNode);
        if (node.numChildren() > 1 && parseChildren.length >= 1) {
            const commonParse = {};
            const conflictingParse = new Set();
            for (const parseNode of parseChildren) {
                const parse = parseNode.parse;
                for (const k of keys(parse)) {
                    if (!(k in commonParse)) {
                        commonParse[k] = parse[k];
                    }
                    else if (commonParse[k] !== parse[k]) {
                        conflictingParse.add(k);
                    }
                }
            }
            for (const field of conflictingParse) {
                delete commonParse[field];
            }
            if (!isEmpty(commonParse)) {
                this.setModified();
                const mergedParseNode = new ParseNode(node, commonParse);
                for (const childNode of originalChildren) {
                    if (childNode instanceof ParseNode) {
                        for (const key of keys(commonParse)) {
                            delete childNode.parse[key];
                        }
                    }
                    node.removeChild(childNode);
                    childNode.parent = mergedParseNode;
                    // remove empty parse nodes
                    if (childNode instanceof ParseNode && keys(childNode.parse).length === 0) {
                        childNode.remove();
                    }
                }
            }
        }
    }
}
/**
 * Repeatedly remove leaf nodes that are not output or facet nodes.
 * The reason is that we don't need subtrees that don't have any output nodes.
 * Facet nodes are needed for the row or column domains.
 */
class RemoveUnusedSubtrees extends BottomUpOptimizer {
    run(node) {
        if (node instanceof OutputNode || node.numChildren() > 0 || node instanceof FacetNode) ;
        else if (node instanceof SourceNode) ;
        else {
            this.setModified();
            node.remove();
        }
    }
}
/**
 * Merge adjacent time unit nodes.
 */
class MergeTimeUnits extends BottomUpOptimizer {
    run(node) {
        const timeUnitChildren = node.children.filter((x) => x instanceof TimeUnitNode);
        const combination = timeUnitChildren.pop();
        for (const timeUnit of timeUnitChildren) {
            this.setModified();
            combination.merge(timeUnit);
        }
    }
}
class MergeAggregates extends BottomUpOptimizer {
    run(node) {
        const aggChildren = node.children.filter((child) => child instanceof AggregateNode);
        // Object which we'll use to map the fields which an aggregate is grouped by to
        // the set of aggregates with that grouping. This is useful as only aggregates
        // with the same group by can be merged
        const groupedAggregates = {};
        // Build groupedAggregates
        for (const agg of aggChildren) {
            const groupBys = hash(agg.groupBy);
            if (!(groupBys in groupedAggregates)) {
                groupedAggregates[groupBys] = [];
            }
            groupedAggregates[groupBys].push(agg);
        }
        // Merge aggregateNodes with same key in groupedAggregates
        for (const group of keys(groupedAggregates)) {
            const mergeableAggs = groupedAggregates[group];
            if (mergeableAggs.length > 1) {
                const mergedAggs = mergeableAggs.pop();
                for (const agg of mergeableAggs) {
                    if (mergedAggs.merge(agg)) {
                        node.removeChild(agg);
                        agg.parent = mergedAggs;
                        agg.remove();
                        this.setModified();
                    }
                }
            }
        }
    }
}
/**
 * Merge bin nodes and move them up through forks. Stop at filters, parse, identifier as we want them to stay before the bin node.
 */
class MergeBins extends BottomUpOptimizer {
    model;
    constructor(model) {
        super();
        this.model = model;
    }
    run(node) {
        const moveBinsUp = !(isDataSourceNode(node) ||
            node instanceof FilterNode ||
            node instanceof ParseNode ||
            node instanceof IdentifierNode);
        const promotableBins = [];
        const remainingBins = [];
        for (const child of node.children) {
            if (child instanceof BinNode) {
                if (moveBinsUp && !fieldIntersection(node.producedFields(), child.dependentFields())) {
                    promotableBins.push(child);
                }
                else {
                    remainingBins.push(child);
                }
            }
        }
        if (promotableBins.length > 0) {
            const promotedBin = promotableBins.pop();
            for (const bin of promotableBins) {
                promotedBin.merge(bin, this.model.renameSignal.bind(this.model));
            }
            this.setModified();
            if (node instanceof BinNode) {
                node.merge(promotedBin, this.model.renameSignal.bind(this.model));
            }
            else {
                promotedBin.swapWithParent();
            }
        }
        if (remainingBins.length > 1) {
            const remainingBin = remainingBins.pop();
            for (const bin of remainingBins) {
                remainingBin.merge(bin, this.model.renameSignal.bind(this.model));
            }
            this.setModified();
        }
    }
}
/**
 * This optimizer takes output nodes that are at a fork and moves them before the fork.
 *
 * The algorithm iterates over the children and tries to find the last output node in a chain of output nodes.
 * It then moves all output nodes before that main output node. All other children (and the children of the output nodes)
 * are inserted after the main output node.
 */
class MergeOutputs extends BottomUpOptimizer {
    run(node) {
        const children = [...node.children];
        const hasOutputChild = some(children, (child) => child instanceof OutputNode);
        if (!hasOutputChild || node.numChildren() <= 1) {
            return;
        }
        const otherChildren = [];
        // The output node we will connect all other nodes to.
        // Output nodes will be added before the new node, other nodes after.
        let mainOutput;
        for (const child of children) {
            if (child instanceof OutputNode) {
                let lastOutput = child;
                while (lastOutput.numChildren() === 1) {
                    const [theChild] = lastOutput.children;
                    if (theChild instanceof OutputNode) {
                        lastOutput = theChild;
                    }
                    else {
                        break;
                    }
                }
                otherChildren.push(...lastOutput.children);
                if (mainOutput) {
                    // Move the output nodes before the mainOutput. We do this by setting
                    // the parent of the first not to the parent of the main output and
                    // the main output's parent to the last output.
                    // note: the child is the first output
                    node.removeChild(child);
                    child.parent = mainOutput.parent;
                    mainOutput.parent.removeChild(mainOutput);
                    mainOutput.parent = lastOutput;
                    this.setModified();
                }
                else {
                    mainOutput = lastOutput;
                }
            }
            else {
                otherChildren.push(child);
            }
        }
        if (otherChildren.length) {
            this.setModified();
            for (const child of otherChildren) {
                child.parent.removeChild(child);
                child.parent = mainOutput;
            }
        }
    }
}

/**
 * A class for the join aggregate transform nodes.
 */
class JoinAggregateTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new JoinAggregateTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
    }
    addDimensions(fields) {
        this.transform.groupby = unique(this.transform.groupby.concat(fields), (d) => d);
    }
    dependentFields() {
        const out = new Set();
        if (this.transform.groupby) {
            this.transform.groupby.forEach(out.add, out);
        }
        this.transform.joinaggregate
            .map((w) => w.field)
            .filter((f) => f !== undefined)
            .forEach(out.add, out);
        return out;
    }
    producedFields() {
        return new Set(this.transform.joinaggregate.map(this.getDefaultName));
    }
    getDefaultName(joinAggregateFieldDef) {
        return joinAggregateFieldDef.as ?? vgField(joinAggregateFieldDef);
    }
    hash() {
        return `JoinAggregateTransform ${hash(this.transform)}`;
    }
    assemble() {
        const fields = [];
        const ops = [];
        const as = [];
        for (const joinaggregate of this.transform.joinaggregate) {
            ops.push(joinaggregate.op);
            as.push(this.getDefaultName(joinaggregate));
            fields.push(joinaggregate.field === undefined ? null : joinaggregate.field);
        }
        const groupby = this.transform.groupby;
        return {
            type: 'joinaggregate',
            as,
            ops,
            fields,
            ...(groupby !== undefined ? { groupby } : {}),
        };
    }
}

class FilterInvalidNode extends DataFlowNode {
    filter;
    clone() {
        return new FilterInvalidNode(null, { ...this.filter });
    }
    constructor(parent, filter) {
        super(parent);
        this.filter = filter;
    }
    static make(parent, model, dataSourcesForHandlingInvalidValues) {
        const { config, markDef } = model;
        const { marks, scales } = dataSourcesForHandlingInvalidValues;
        if (marks === 'include-invalid-values' && scales === 'include-invalid-values') {
            // If neither marks nor scale domains need data source to filter null values, then don't add the filter.
            return null;
        }
        const filter = model.reduceFieldDef((aggregator, fieldDef, channel) => {
            const scaleComponent = isScaleChannel(channel) && model.getScaleComponent(channel);
            if (scaleComponent) {
                const scaleType = scaleComponent.get('type');
                const { aggregate } = fieldDef;
                const invalidDataMode = getScaleInvalidDataMode({
                    scaleChannel: channel,
                    markDef,
                    config,
                    scaleType,
                    isCountAggregate: isCountingAggregateOp(aggregate),
                });
                // If the invalid data mode is include or always-valid, we don't need to filter invalid values as the scale can handle invalid values.
                if (invalidDataMode !== 'show' && invalidDataMode !== 'always-valid') {
                    aggregator[fieldDef.field] = fieldDef; // we know that the fieldDef is a typed field def
                }
            }
            return aggregator;
        }, {});
        if (!keys(filter).length) {
            return null;
        }
        return new FilterInvalidNode(parent, filter);
    }
    dependentFields() {
        return new Set(keys(this.filter));
    }
    producedFields() {
        return new Set(); // filter does not produce any new fields
    }
    hash() {
        return `FilterInvalid ${hash(this.filter)}`;
    }
    /**
     * Create the VgTransforms for each of the filtered fields.
     */
    assemble() {
        const filters = keys(this.filter).reduce((vegaFilters, field) => {
            const fieldDef = this.filter[field];
            const ref = vgField(fieldDef, { expr: 'datum' });
            if (fieldDef !== null) {
                if (fieldDef.type === 'temporal') {
                    vegaFilters.push(`(isDate(${ref}) || (${isValidFiniteNumberExpr(ref)}))`);
                }
                else if (fieldDef.type === 'quantitative') {
                    vegaFilters.push(isValidFiniteNumberExpr(ref));
                }
                else ;
            }
            return vegaFilters;
        }, []);
        return filters.length > 0
            ? {
                type: 'filter',
                expr: filters.join(' && '),
            }
            : null;
    }
}
function isValidFiniteNumberExpr(ref) {
    return `isValid(${ref}) && isFinite(+${ref})`;
}

function getStackByFields(model) {
    return model.stack.stackBy.reduce((fields, by) => {
        const fieldDef = by.fieldDef;
        const _field = vgField(fieldDef);
        if (_field) {
            fields.push(_field);
        }
        return fields;
    }, []);
}
function isValidAsArray(as) {
    return isArray(as) && as.every((s) => isString(s)) && as.length > 1;
}
class StackNode extends DataFlowNode {
    _stack;
    clone() {
        return new StackNode(null, duplicate(this._stack));
    }
    constructor(parent, stack) {
        super(parent);
        this._stack = stack;
    }
    static makeFromTransform(parent, stackTransform) {
        const { stack, groupby, as, offset = 'zero' } = stackTransform;
        const sortFields = [];
        const sortOrder = [];
        if (stackTransform.sort !== undefined) {
            for (const sortField of stackTransform.sort) {
                sortFields.push(sortField.field);
                sortOrder.push(getFirstDefined(sortField.order, 'ascending'));
            }
        }
        const sort = {
            field: sortFields,
            order: sortOrder,
        };
        let normalizedAs;
        if (isValidAsArray(as)) {
            normalizedAs = as;
        }
        else if (isString(as)) {
            normalizedAs = [as, `${as}_end`];
        }
        else {
            normalizedAs = [`${stackTransform.stack}_start`, `${stackTransform.stack}_end`];
        }
        return new StackNode(parent, {
            dimensionFieldDefs: [],
            stackField: stack,
            groupby,
            offset,
            sort,
            facetby: [],
            as: normalizedAs,
        });
    }
    static makeFromEncoding(parent, model) {
        const stackProperties = model.stack;
        const { encoding } = model;
        if (!stackProperties) {
            return null;
        }
        const { groupbyChannels, fieldChannel, offset, impute } = stackProperties;
        const dimensionFieldDefs = groupbyChannels
            .map((groupbyChannel) => {
            const cDef = encoding[groupbyChannel];
            return getFieldDef(cDef);
        })
            .filter((def) => !!def);
        const stackby = getStackByFields(model);
        const orderDef = model.encoding.order;
        let sort;
        if (isArray(orderDef) || isFieldDef(orderDef)) {
            sort = sortParams(orderDef);
        }
        else {
            const sortOrder = isOrderOnlyDef(orderDef) ? orderDef.sort : fieldChannel === 'y' ? 'descending' : 'ascending';
            // default = descending by stackFields
            // FIXME is the default here correct for binned fields?
            sort = stackby.reduce((s, field) => {
                if (!s.field.includes(field)) {
                    s.field.push(field);
                    s.order.push(sortOrder);
                }
                return s;
            }, { field: [], order: [] });
        }
        return new StackNode(parent, {
            dimensionFieldDefs,
            stackField: model.vgField(fieldChannel),
            facetby: [],
            stackby,
            sort,
            offset,
            impute,
            as: [
                model.vgField(fieldChannel, { suffix: 'start', forAs: true }),
                model.vgField(fieldChannel, { suffix: 'end', forAs: true }),
            ],
        });
    }
    get stack() {
        return this._stack;
    }
    addDimensions(fields) {
        this._stack.facetby.push(...fields);
    }
    dependentFields() {
        const out = new Set();
        out.add(this._stack.stackField);
        this.getGroupbyFields().forEach(out.add, out);
        this._stack.facetby.forEach(out.add, out);
        this._stack.sort.field.forEach(out.add, out);
        return out;
    }
    producedFields() {
        return new Set(this._stack.as);
    }
    hash() {
        return `Stack ${hash(this._stack)}`;
    }
    getGroupbyFields() {
        const { dimensionFieldDefs, impute, groupby } = this._stack;
        if (dimensionFieldDefs.length > 0) {
            return dimensionFieldDefs
                .map((dimensionFieldDef) => {
                if (dimensionFieldDef.bin) {
                    if (impute) {
                        // For binned group by field with impute, we calculate bin_mid
                        // as we cannot impute two fields simultaneously
                        return [vgField(dimensionFieldDef, { binSuffix: 'mid' })];
                    }
                    return [
                        // For binned group by field without impute, we need both bin (start) and bin_end
                        vgField(dimensionFieldDef, {}),
                        vgField(dimensionFieldDef, { binSuffix: 'end' }),
                    ];
                }
                return [vgField(dimensionFieldDef)];
            })
                .flat();
        }
        return groupby ?? [];
    }
    assemble() {
        const transform = [];
        const { facetby, dimensionFieldDefs, stackField: field, stackby, sort, offset, impute, as } = this._stack;
        // Impute
        if (impute) {
            for (const dimensionFieldDef of dimensionFieldDefs) {
                const { bandPosition = 0.5, bin } = dimensionFieldDef;
                if (bin) {
                    // As we can only impute one field at a time, we need to calculate
                    // mid point for a binned field
                    const binStart = vgField(dimensionFieldDef, { expr: 'datum' });
                    const binEnd = vgField(dimensionFieldDef, { expr: 'datum', binSuffix: 'end' });
                    transform.push({
                        type: 'formula',
                        expr: `${isValidFiniteNumberExpr(binStart)} ? ${bandPosition}*${binStart}+${1 - bandPosition}*${binEnd} : ${binStart}`,
                        as: vgField(dimensionFieldDef, { binSuffix: 'mid', forAs: true }),
                    });
                }
                transform.push({
                    type: 'impute',
                    field,
                    groupby: [...stackby, ...facetby],
                    key: vgField(dimensionFieldDef, { binSuffix: 'mid' }),
                    method: 'value',
                    value: 0,
                });
            }
        }
        // Stack
        transform.push({
            type: 'stack',
            groupby: [...this.getGroupbyFields(), ...facetby],
            field,
            sort,
            as,
            offset,
        });
        return transform;
    }
}

/**
 * A class for the window transform nodes
 */
class WindowTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new WindowTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
    }
    addDimensions(fields) {
        this.transform.groupby = unique(this.transform.groupby.concat(fields), (d) => d);
    }
    dependentFields() {
        const out = new Set();
        (this.transform.groupby ?? []).forEach(out.add, out);
        (this.transform.sort ?? []).forEach((m) => out.add(m.field));
        this.transform.window
            .map((w) => w.field)
            .filter((f) => f !== undefined)
            .forEach(out.add, out);
        return out;
    }
    producedFields() {
        return new Set(this.transform.window.map(this.getDefaultName));
    }
    getDefaultName(windowFieldDef) {
        return windowFieldDef.as ?? vgField(windowFieldDef);
    }
    hash() {
        return `WindowTransform ${hash(this.transform)}`;
    }
    assemble() {
        const fields = [];
        const ops = [];
        const as = [];
        const params = [];
        for (const window of this.transform.window) {
            ops.push(window.op);
            as.push(this.getDefaultName(window));
            params.push(window.param === undefined ? null : window.param);
            fields.push(window.field === undefined ? null : window.field);
        }
        const frame = this.transform.frame;
        const groupby = this.transform.groupby;
        if (frame && frame[0] === null && frame[1] === null && ops.every((o) => isAggregateOp(o))) {
            // when the window does not rely on any particular window ops or frame, switch to a simpler and more efficient joinaggregate
            return {
                type: 'joinaggregate',
                as,
                ops: ops,
                fields,
                ...(groupby !== undefined ? { groupby } : {}),
            };
        }
        const sortFields = [];
        const sortOrder = [];
        if (this.transform.sort !== undefined) {
            for (const sortField of this.transform.sort) {
                sortFields.push(sortField.field);
                sortOrder.push(sortField.order ?? 'ascending');
            }
        }
        const sort = {
            field: sortFields,
            order: sortOrder,
        };
        const ignorePeers = this.transform.ignorePeers;
        return {
            type: 'window',
            params,
            as,
            ops,
            fields,
            sort,
            ...(ignorePeers !== undefined ? { ignorePeers } : {}),
            ...(groupby !== undefined ? { groupby } : {}),
            ...(frame !== undefined ? { frame } : {}),
        };
    }
}

/**
 * Clones the subtree and ignores output nodes except for the leaves, which are renamed.
 */
function cloneSubtree(facet) {
    function clone(node) {
        if (!(node instanceof FacetNode)) {
            const copy = node.clone();
            if (copy instanceof OutputNode) {
                const newName = FACET_SCALE_PREFIX + copy.getSource();
                copy.setSource(newName);
                facet.model.component.data.outputNodes[newName] = copy;
            }
            else if (copy instanceof AggregateNode ||
                copy instanceof StackNode ||
                copy instanceof WindowTransformNode ||
                copy instanceof JoinAggregateTransformNode) {
                copy.addDimensions(facet.fields);
            }
            for (const n of node.children.flatMap(clone)) {
                n.parent = copy;
            }
            return [copy];
        }
        return node.children.flatMap(clone);
    }
    return clone;
}
/**
 * Move facet nodes down to the next fork or output node. Also pull the main output with the facet node.
 * After moving down the facet node, make a copy of the subtree and make it a child of the main output.
 */
function moveFacetDown(node) {
    if (node instanceof FacetNode) {
        if (node.numChildren() === 1 && !(node.children[0] instanceof OutputNode)) {
            // move down until we hit a fork or output node
            const child = node.children[0];
            if (child instanceof AggregateNode ||
                child instanceof StackNode ||
                child instanceof WindowTransformNode ||
                child instanceof JoinAggregateTransformNode) {
                child.addDimensions(node.fields);
            }
            child.swapWithParent();
            moveFacetDown(node);
        }
        else {
            // move main to facet
            const facetMain = node.model.component.data.main;
            moveMainDownToFacet(facetMain);
            // replicate the subtree and place it before the facet's main node
            const cloner = cloneSubtree(node);
            const copy = node.children.map(cloner).flat();
            for (const c of copy) {
                c.parent = facetMain;
            }
        }
    }
    else {
        node.children.map(moveFacetDown);
    }
}
function moveMainDownToFacet(node) {
    if (node instanceof OutputNode && node.type === DataSourceType.Main) {
        if (node.numChildren() === 1) {
            const child = node.children[0];
            if (!(child instanceof FacetNode)) {
                child.swapWithParent();
                moveMainDownToFacet(node);
            }
        }
    }
}

const FACET_SCALE_PREFIX = 'scale_';
const MAX_OPTIMIZATION_RUNS = 5;
/**
 * Iterates over a dataflow graph and checks whether all links are consistent.
 */
function checkLinks(nodes) {
    for (const node of nodes) {
        for (const child of node.children) {
            if (child.parent !== node) {
                // log.error('Dataflow graph is inconsistent.', node, child);
                return false;
            }
        }
        if (!checkLinks(node.children)) {
            return false;
        }
    }
    return true;
}
/**
 * Run the specified optimizer on the provided nodes.
 *
 * @param optimizer The optimizer instance to run.
 * @param nodes A set of nodes to optimize.
 */
function runOptimizer(optimizer, nodes) {
    let modified = false;
    for (const node of nodes) {
        modified = optimizer.optimize(node) || modified;
    }
    return modified;
}
function optimizationDataflowHelper(dataComponent, model, firstPass) {
    let roots = dataComponent.sources;
    let modified = false;
    modified = runOptimizer(new RemoveUnnecessaryOutputNodes(), roots) || modified;
    modified = runOptimizer(new RemoveUnnecessaryIdentifierNodes(model), roots) || modified;
    // remove source nodes that don't have any children because they also don't have output nodes
    roots = roots.filter((r) => r.numChildren() > 0);
    modified = runOptimizer(new RemoveUnusedSubtrees(), roots) || modified;
    roots = roots.filter((r) => r.numChildren() > 0);
    if (!firstPass) {
        // Only run these optimizations after the optimizer has moved down the facet node.
        // With this change, we can be more aggressive in the optimizations.
        modified = runOptimizer(new MoveParseUp(), roots) || modified;
        modified = runOptimizer(new MergeBins(model), roots) || modified;
        modified = runOptimizer(new RemoveDuplicateTimeUnits(), roots) || modified;
        modified = runOptimizer(new MergeParse(), roots) || modified;
        modified = runOptimizer(new MergeAggregates(), roots) || modified;
        modified = runOptimizer(new MergeTimeUnits(), roots) || modified;
        modified = runOptimizer(new MergeIdenticalNodes(), roots) || modified;
        modified = runOptimizer(new MergeOutputs(), roots) || modified;
    }
    dataComponent.sources = roots;
    return modified;
}
/**
 * Optimizes the dataflow of the passed in data component.
 */
function optimizeDataflow(data, model) {
    // check before optimizations
    checkLinks(data.sources);
    let firstPassCounter = 0;
    let secondPassCounter = 0;
    for (let i = 0; i < MAX_OPTIMIZATION_RUNS; i++) {
        if (!optimizationDataflowHelper(data, model, true)) {
            break;
        }
        firstPassCounter++;
    }
    // move facets down and make a copy of the subtree so that we can have scales at the top level
    data.sources.map(moveFacetDown);
    for (let i = 0; i < MAX_OPTIMIZATION_RUNS; i++) {
        if (!optimizationDataflowHelper(data, model, false)) {
            break;
        }
        secondPassCounter++;
    }
    // check after optimizations
    checkLinks(data.sources);
    if (Math.max(firstPassCounter, secondPassCounter) === MAX_OPTIMIZATION_RUNS) {
        warn(`Maximum optimization runs(${MAX_OPTIMIZATION_RUNS}) reached.`);
    }
}

/**
 * A class that behaves like a SignalRef but lazily generates the signal.
 * The provided generator function should use `Model.getSignalName` to use the correct signal name.
 */
class SignalRefWrapper {
    constructor(exprGenerator) {
        Object.defineProperty(this, 'signal', {
            enumerable: true,
            get: exprGenerator,
        });
    }
    signal; // for ts
    static fromName(rename, signalName) {
        return new SignalRefWrapper(() => rename(signalName));
    }
}

function parseScaleDomain(model) {
    if (isUnitModel(model)) {
        parseUnitScaleDomain(model);
    }
    else {
        parseNonUnitScaleDomain(model);
    }
}
function parseUnitScaleDomain(model) {
    const localScaleComponents = model.component.scales;
    for (const channel of keys(localScaleComponents)) {
        const domains = parseDomainForChannel(model, channel);
        const localScaleCmpt = localScaleComponents[channel];
        localScaleCmpt.setWithExplicit('domains', domains);
        parseSelectionDomain(model, channel);
        if (model.component.data.isFaceted) {
            // get resolve from closest facet parent as this decides whether we need to refer to cloned subtree or not
            let facetParent = model;
            while (!isFacetModel(facetParent) && facetParent.parent) {
                facetParent = facetParent.parent;
            }
            const resolve = facetParent.component.resolve.scale[channel];
            if (resolve === 'shared') {
                for (const domain of domains.value) {
                    // Replace the scale domain with data output from a cloned subtree after the facet.
                    if (isDataRefDomain(domain)) {
                        // use data from cloned subtree (which is the same as data but with a prefix added once)
                        domain.data = FACET_SCALE_PREFIX + domain.data.replace(FACET_SCALE_PREFIX, '');
                    }
                }
            }
        }
    }
}
function parseNonUnitScaleDomain(model) {
    for (const child of model.children) {
        parseScaleDomain(child);
    }
    const localScaleComponents = model.component.scales;
    for (const channel of keys(localScaleComponents)) {
        let domains;
        let selectionExtent = null;
        for (const child of model.children) {
            const childComponent = child.component.scales[channel];
            if (childComponent) {
                if (domains === undefined) {
                    domains = childComponent.getWithExplicit('domains');
                }
                else {
                    domains = mergeValuesWithExplicit(domains, childComponent.getWithExplicit('domains'), 'domains', 'scale', domainsTieBreaker);
                }
                const se = childComponent.get('selectionExtent');
                if (selectionExtent && se && selectionExtent.param !== se.param) {
                    warn(NEEDS_SAME_SELECTION);
                }
                selectionExtent = se;
            }
        }
        localScaleComponents[channel].setWithExplicit('domains', domains);
        if (selectionExtent) {
            localScaleComponents[channel].set('selectionExtent', selectionExtent, true);
        }
    }
}
/**
 * Remove unaggregated domain if it is not applicable
 * Add unaggregated domain if domain is not specified and config.scale.useUnaggregatedDomain is true.
 */
function normalizeUnaggregatedDomain(domain, fieldDef, scaleType, scaleConfig) {
    if (domain === 'unaggregated') {
        const { valid, reason } = canUseUnaggregatedDomain(fieldDef, scaleType);
        if (!valid) {
            warn(reason);
            return undefined;
        }
    }
    else if (domain === undefined && scaleConfig.useUnaggregatedDomain) {
        // Apply config if domain is not specified.
        const { valid } = canUseUnaggregatedDomain(fieldDef, scaleType);
        if (valid) {
            return 'unaggregated';
        }
    }
    return domain;
}
function parseDomainForChannel(model, channel) {
    const scaleType = model.getScaleComponent(channel).get('type');
    const { encoding } = model;
    const domain = normalizeUnaggregatedDomain(model.scaleDomain(channel), model.typedFieldDef(channel), scaleType, model.config.scale);
    if (domain !== model.scaleDomain(channel)) {
        model.specifiedScales[channel] = {
            ...model.specifiedScales[channel],
            domain,
        };
    }
    // If channel is either X or Y then union them with X2 & Y2 if they exist
    if (channel === 'x' && getFieldOrDatumDef(encoding.x2)) {
        if (getFieldOrDatumDef(encoding.x)) {
            return mergeValuesWithExplicit(parseSingleChannelDomain(scaleType, domain, model, 'x'), parseSingleChannelDomain(scaleType, domain, model, 'x2'), 'domain', 'scale', domainsTieBreaker);
        }
        else {
            return parseSingleChannelDomain(scaleType, domain, model, 'x2');
        }
    }
    else if (channel === 'y' && getFieldOrDatumDef(encoding.y2)) {
        if (getFieldOrDatumDef(encoding.y)) {
            return mergeValuesWithExplicit(parseSingleChannelDomain(scaleType, domain, model, 'y'), parseSingleChannelDomain(scaleType, domain, model, 'y2'), 'domain', 'scale', domainsTieBreaker);
        }
        else {
            return parseSingleChannelDomain(scaleType, domain, model, 'y2');
        }
    }
    return parseSingleChannelDomain(scaleType, domain, model, channel);
}
function mapDomainToDataSignal(domain, type, timeUnit) {
    return domain.map((v) => {
        const data = valueExpr(v, { timeUnit, type });
        return { signal: `{data: ${data}}` };
    });
}
function convertDomainIfItIsDateTime(domain, type, timeUnit) {
    // explicit value
    const normalizedTimeUnit = normalizeTimeUnit(timeUnit)?.unit;
    if (type === 'temporal' || normalizedTimeUnit) {
        return mapDomainToDataSignal(domain, type, normalizedTimeUnit);
    }
    return [domain]; // Date time won't make sense
}
function parseSingleChannelDomain(scaleType, domain, model, channel) {
    const { encoding, markDef, mark, config, stack } = model;
    const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]);
    const { type } = fieldOrDatumDef;
    const timeUnit = fieldOrDatumDef['timeUnit'];
    const dataSourceTypeForScaleDomain = getScaleDataSourceForHandlingInvalidValues({
        invalid: getMarkConfig('invalid', markDef, config),
        isPath: isPathMark(mark),
    });
    if (isDomainUnionWith(domain)) {
        const defaultDomain = parseSingleChannelDomain(scaleType, undefined, model, channel);
        const unionWith = convertDomainIfItIsDateTime(domain.unionWith, type, timeUnit);
        return makeExplicit([...unionWith, ...defaultDomain.value]);
    }
    else if (isSignalRef(domain)) {
        return makeExplicit([domain]);
    }
    else if (domain && domain !== 'unaggregated' && !isParameterDomain(domain)) {
        return makeExplicit(convertDomainIfItIsDateTime(domain, type, timeUnit));
    }
    if (stack && channel === stack.fieldChannel) {
        if (stack.offset === 'normalize') {
            return makeImplicit([[0, 1]]);
        }
        const data = model.requestDataName(dataSourceTypeForScaleDomain);
        return makeImplicit([
            {
                data,
                field: model.vgField(channel, { suffix: 'start' }),
            },
            {
                data,
                field: model.vgField(channel, { suffix: 'end' }),
            },
        ]);
    }
    const sort = isScaleChannel(channel) && isFieldDef(fieldOrDatumDef) ? domainSort(model, channel, scaleType) : undefined;
    if (isDatumDef(fieldOrDatumDef)) {
        const d = convertDomainIfItIsDateTime([fieldOrDatumDef.datum], type, timeUnit);
        return makeImplicit(d);
    }
    const fieldDef = fieldOrDatumDef; // now we can be sure it's a fieldDef
    if (domain === 'unaggregated') {
        const { field } = fieldOrDatumDef;
        return makeImplicit([
            {
                data: model.requestDataName(dataSourceTypeForScaleDomain),
                field: vgField({ field, aggregate: 'min' }),
            },
            {
                data: model.requestDataName(dataSourceTypeForScaleDomain),
                field: vgField({ field, aggregate: 'max' }),
            },
        ]);
    }
    else if (isBinning(fieldDef.bin)) {
        if (hasDiscreteDomain(scaleType)) {
            if (scaleType === 'bin-ordinal') {
                // we can omit the domain as it is inferred from the `bins` property
                return makeImplicit([]);
            }
            // ordinal bin scale takes domain from bin_range, ordered by bin start
            // This is useful for both axis-based scale (x/y) and legend-based scale (other channels).
            return makeImplicit([
                {
                    // If sort by aggregation of a specified sort field, we need to use RAW table,
                    // so we can aggregate values for the scale independently from the main aggregation.
                    data: isBoolean(sort)
                        ? model.requestDataName(dataSourceTypeForScaleDomain)
                        : model.requestDataName(DataSourceType.Raw),
                    // Use range if we added it and the scale does not support computing a range as a signal.
                    field: model.vgField(channel, binRequiresRange(fieldDef, channel) ? { binSuffix: 'range' } : {}),
                    // we have to use a sort object if sort = true to make the sort correct by bin start
                    sort: sort === true || !isObject(sort)
                        ? {
                            field: model.vgField(channel, {}),
                            op: 'min',
                        }
                        : sort,
                },
            ]);
        }
        else {
            // continuous scales
            const { bin } = fieldDef;
            if (isBinning(bin)) {
                const binSignal = getBinSignalName(model, fieldDef.field, bin);
                return makeImplicit([
                    new SignalRefWrapper(() => {
                        const signal = model.getSignalName(binSignal);
                        return `[${signal}.start, ${signal}.stop]`;
                    }),
                ]);
            }
            else {
                return makeImplicit([
                    {
                        data: model.requestDataName(dataSourceTypeForScaleDomain),
                        field: model.vgField(channel, {}),
                    },
                ]);
            }
        }
    }
    else if (fieldDef.timeUnit && contains(['time', 'utc'], scaleType)) {
        const fieldDef2 = encoding[getSecondaryRangeChannel(channel)];
        if (hasBandEnd(fieldDef, fieldDef2, markDef, config)) {
            const data = model.requestDataName(dataSourceTypeForScaleDomain);
            const bandPosition = getBandPosition({ fieldDef, fieldDef2, markDef, config });
            const isRectWithOffset = isRectBasedMark(mark) && bandPosition !== 0.5 && isXorY(channel);
            return makeImplicit([
                {
                    data,
                    field: model.vgField(channel, isRectWithOffset ? { suffix: OFFSETTED_RECT_START_SUFFIX } : {}),
                },
                {
                    data,
                    field: model.vgField(channel, { suffix: isRectWithOffset ? OFFSETTED_RECT_END_SUFFIX : 'end' }),
                },
            ]);
        }
    }
    if (sort) {
        return makeImplicit([
            {
                // If sort by aggregation of a specified sort field, we need to use RAW table,
                // so we can aggregate values for the scale independently from the main aggregation.
                data: isBoolean(sort)
                    ? model.requestDataName(dataSourceTypeForScaleDomain)
                    : model.requestDataName(DataSourceType.Raw),
                field: model.vgField(channel),
                sort,
            },
        ]);
    }
    else {
        return makeImplicit([
            {
                data: model.requestDataName(dataSourceTypeForScaleDomain),
                field: model.vgField(channel),
            },
        ]);
    }
}
function normalizeSortField(sort, isStackedMeasure) {
    const { op, field, order } = sort;
    return {
        // Apply default op
        op: op ?? (isStackedMeasure ? 'sum' : DEFAULT_SORT_OP),
        // flatten nested fields
        ...(field ? { field: replacePathInField(field) } : {}),
        ...(order ? { order } : {}),
    };
}
function parseSelectionDomain(model, channel) {
    const scale = model.component.scales[channel];
    const spec = model.specifiedScales[channel].domain;
    const bin = model.fieldDef(channel)?.bin;
    const domain = isParameterDomain(spec) ? spec : undefined;
    const extent = isBinParams(bin) && isParameterExtent(bin.extent) ? bin.extent : undefined;
    if (domain || extent) {
        // As scale parsing occurs before selection parsing, we cannot set
        // domainRaw directly. So instead, we store the selectionExtent on
        // the scale component, and then add domainRaw during scale assembly.
        scale.set('selectionExtent', domain ?? extent, true);
    }
}
function domainSort(model, channel, scaleType) {
    if (!hasDiscreteDomain(scaleType)) {
        return undefined;
    }
    // save to cast as the only exception is the geojson type for shape, which would not generate a scale
    const fieldDef = model.fieldDef(channel);
    const sort = fieldDef.sort;
    // if the sort is specified with array, use the derived sort index field
    if (isSortArray(sort)) {
        return {
            op: 'min',
            field: sortArrayIndexField(fieldDef, channel),
            order: 'ascending',
        };
    }
    const { stack } = model;
    const stackDimensions = stack
        ? new Set([...stack.groupbyFields, ...stack.stackBy.map((s) => s.fieldDef.field)])
        : undefined;
    // Sorted based on an aggregate calculation over a specified sort field (only for ordinal scale)
    if (isSortField(sort)) {
        const isStackedMeasure = stack && !stackDimensions.has(sort.field);
        return normalizeSortField(sort, isStackedMeasure);
    }
    else if (isSortByEncoding(sort)) {
        const { encoding, order } = sort;
        const fieldDefToSortBy = model.fieldDef(encoding);
        const { aggregate, field } = fieldDefToSortBy;
        const isStackedMeasure = stack && !stackDimensions.has(field);
        if (isArgminDef(aggregate) || isArgmaxDef(aggregate)) {
            return normalizeSortField({
                field: vgField(fieldDefToSortBy),
                order,
            }, isStackedMeasure);
        }
        else if (isAggregateOp(aggregate) || !aggregate) {
            return normalizeSortField({
                op: aggregate, // can't be argmin/argmax since we don't support them in encoding field def
                field,
                order,
            }, isStackedMeasure);
        }
    }
    else if (sort === 'descending') {
        return {
            op: 'min',
            field: model.vgField(channel),
            order: 'descending',
        };
    }
    else if (contains(['ascending', undefined /* default =ascending*/], sort)) {
        return true;
    }
    // sort == null
    return undefined;
}
/**
 * Determine if a scale can use unaggregated domain.
 * @return {Boolean} Returns true if all of the following conditions apply:
 * 1. `scale.domain` is `unaggregated`
 * 2. Aggregation function is not `count` or `sum`
 * 3. The scale is quantitative or time scale.
 */
function canUseUnaggregatedDomain(fieldDef, scaleType) {
    const { aggregate, type } = fieldDef;
    if (!aggregate) {
        return {
            valid: false,
            reason: unaggregateDomainHasNoEffectForRawField(fieldDef),
        };
    }
    if (isString(aggregate) && !SHARED_DOMAIN_OPS.has(aggregate)) {
        return {
            valid: false,
            reason: unaggregateDomainWithNonSharedDomainOp(aggregate),
        };
    }
    if (type === 'quantitative') {
        if (scaleType === 'log') {
            return {
                valid: false,
                reason: unaggregatedDomainWithLogScale(fieldDef),
            };
        }
    }
    return { valid: true };
}
/**
 * Tie breaker for mergeValuesWithExplicit for domains. We concat the specified values.
 */
function domainsTieBreaker(v1, v2, property, propertyOf) {
    if (v1.explicit && v2.explicit) {
        warn(mergeConflictingDomainProperty(property, propertyOf, v1.value, v2.value));
    }
    // If equal score, concat the domains so that we union them later.
    return { explicit: v1.explicit, value: [...v1.value, ...v2.value] };
}
/**
 * Converts an array of domains to a single Vega scale domain.
 */
function mergeDomains(domains) {
    const uniqueDomains = unique(domains.map((domain) => {
        // ignore sort property when computing the unique domains
        if (isDataRefDomain(domain)) {
            const { sort: _s, ...domainWithoutSort } = domain;
            return domainWithoutSort;
        }
        return domain;
    }), hash);
    const sorts = unique(domains
        .map((d) => {
        if (isDataRefDomain(d)) {
            const s = d.sort;
            if (s !== undefined && !isBoolean(s)) {
                if ('op' in s && s.op === 'count') {
                    // let's make sure that if op is count, we don't use a field
                    delete s.field;
                }
                if (s.order === 'ascending') {
                    // drop order: ascending as it is the default
                    delete s.order;
                }
            }
            return s;
        }
        return undefined;
    })
        .filter((s) => s !== undefined), hash);
    if (uniqueDomains.length === 0) {
        return undefined;
    }
    else if (uniqueDomains.length === 1) {
        const domain = domains[0];
        if (isDataRefDomain(domain) && sorts.length > 0) {
            let sort = sorts[0];
            if (sorts.length > 1) {
                warn(MORE_THAN_ONE_SORT);
                // Get sorts with non-default ops
                const filteredSorts = sorts.filter((s) => isObject(s) && 'op' in s && s.op !== 'min');
                if (sorts.every((s) => isObject(s) && 'op' in s) && filteredSorts.length === 1) {
                    sort = filteredSorts[0];
                }
                else {
                    sort = true;
                }
            }
            else {
                // Simplify domain sort by removing field and op when the field is the same as the domain field.
                if (isObject(sort) && 'field' in sort) {
                    const sortField = sort.field;
                    if (domain.field === sortField) {
                        sort = sort.order ? { order: sort.order } : true;
                    }
                }
            }
            return {
                ...domain,
                sort,
            };
        }
        return domain;
    }
    // only keep sort properties that work with unioned domains
    const unionDomainSorts = unique(sorts.map((s) => {
        if (isBoolean(s) || !('op' in s) || (isString(s.op) && has$1(MULTIDOMAIN_SORT_OP_INDEX, s.op))) {
            return s;
        }
        warn(domainSortDropped(s));
        return true;
    }), hash);
    let sort;
    if (unionDomainSorts.length === 1) {
        sort = unionDomainSorts[0];
    }
    else if (unionDomainSorts.length > 1) {
        warn(MORE_THAN_ONE_SORT);
        sort = true;
    }
    const allData = unique(domains.map((d) => {
        if (isDataRefDomain(d)) {
            return d.data;
        }
        return null;
    }), (x) => x);
    if (allData.length === 1 && allData[0] !== null) {
        // create a union domain of different fields with a single data source
        const domain = {
            data: allData[0],
            fields: uniqueDomains.map((d) => d.field),
            ...(sort ? { sort } : {}),
        };
        return domain;
    }
    return { fields: uniqueDomains, ...(sort ? { sort } : {}) };
}
/**
 * Return a field if a scale uses a single field.
 * Return `undefined` otherwise.
 */
function getFieldFromDomain(domain) {
    if (isDataRefDomain(domain) && isString(domain.field)) {
        return domain.field;
    }
    else if (isDataRefUnionedDomain(domain)) {
        let field;
        for (const nonUnionDomain of domain.fields) {
            if (isDataRefDomain(nonUnionDomain) && isString(nonUnionDomain.field)) {
                if (!field) {
                    field = nonUnionDomain.field;
                }
                else if (field !== nonUnionDomain.field) {
                    warn(FACETED_INDEPENDENT_DIFFERENT_SOURCES);
                    return field;
                }
            }
        }
        warn(FACETED_INDEPENDENT_SAME_FIELDS_DIFFERENT_SOURCES);
        return field;
    }
    else if (isFieldRefUnionDomain(domain)) {
        warn(FACETED_INDEPENDENT_SAME_SOURCE);
        const field = domain.fields[0];
        return isString(field) ? field : undefined;
    }
    return undefined;
}
function assembleDomain(model, channel) {
    const scaleComponent = model.component.scales[channel];
    const domains = scaleComponent.get('domains').map((domain) => {
        // Correct references to data as the original domain's data was determined
        // in parseScale, which happens before parseData. Thus the original data
        // reference can be incorrect.
        if (isDataRefDomain(domain)) {
            domain.data = model.lookupDataSource(domain.data);
        }
        return domain;
    });
    // domains is an array that has to be merged into a single vega domain
    return mergeDomains(domains);
}

function setLegendEncode(legend, part, vgProp, vgRef) {
    legend.encode ??= {};
    legend.encode[part] ??= {};
    legend.encode[part].update ??= {};
    // @ts-expect-error expression is too complex for typescript to understand
    legend.encode[part].update[vgProp] = vgRef;
}
/**
 * Determines the underlying field name for a given scale channel within a model hierarchy.
 * @param model - The model to search for the field definition
 * @param channel - The scale channel (e.g., 'color', 'size', 'shape') to find the field for
 * @returns The field name if found; otherwise undefined
 */
function getFieldKeyForChannel(model, channel) {
    if (isUnitModel(model)) {
        const fd = model.fieldDef(channel);
        if (fd?.field) {
            return fd.field;
        }
    }
    // Use explicit fields from children
    const childFields = (model.children ?? [])
        .map((child) => getFieldKeyForChannel(child, channel))
        .filter((f) => !!f);
    if (childFields.length > 0) {
        const unique$1 = unique(childFields, hash);
        if (unique$1.length === 1) {
            return unique$1[0];
        }
        return undefined;
    }
    return undefined;
}
function legendsAreMergeCompatible(model, channelA, channelB) {
    if (channelA === channelB)
        return true;
    const typeA = model.getScaleType(channelA);
    const typeB = model.getScaleType(channelB);
    if (!typeA || !typeB)
        return false;
    // Only require discrete/continuous compatibility here. Domain handling is done later.
    const aIsDiscrete = hasDiscreteDomain(typeA);
    const bIsDiscrete = hasDiscreteDomain(typeB);
    return aIsDiscrete === bIsDiscrete;
}
function getLegendGroupKey(fieldKey, channel) {
    return fieldKey ? `field:${fieldKey}` : `channel:${String(channel)}`;
}
function extractDiscreteValuesFromDomain(domain) {
    if (isArray(domain)) {
        const primitives = domain.filter(isPrimitive);
        return primitives.length > 0 ? primitives : null;
    }
    if (isDataRefUnionedDomain(domain)) {
        const values = [];
        values.push(...domain.fields.flatMap((f) => (isArray(f) ? f.filter(isPrimitive) : [])));
        if (values.length > 0) {
            return unique(values, hash);
        }
    }
    return null;
}
/**
 * Compute the union of discrete values from the domains of two channels.
 *
 * @param model - The model to compute the union of discrete values for
 * @param channelA - The first channel to compute the union of discrete values for
 * @param channelB - The second channel to compute the union of discrete values for
 * @returns The union of discrete values
 */
function getDiscreteValuesForChannel(model, channel) {
    try {
        const domain = assembleDomain(model, channel);
        return extractDiscreteValuesFromDomain(domain);
    }
    catch {
        return null;
    }
}
function unionDiscreteValuesForChannels(model, channelA, channelB) {
    const vA = getDiscreteValuesForChannel(model, channelA);
    const vB = getDiscreteValuesForChannel(model, channelB);
    return vA && vB ? unique([...vA, ...vB], hash) : null;
}
function setImplicitLegendValues(cmpt, values, warnMessage) {
    if (values && values.length > 0) {
        const valuesProp = cmpt.getWithExplicit('values');
        if (!valuesProp?.explicit) {
            if (warnMessage) {
                warn(warnMessage);
            }
            cmpt.set('values', values, false);
        }
    }
}
function domainsExplicitAndEqual(model, channelA, channelB) {
    const scA = model.getScaleComponent(channelA);
    const scB = model.getScaleComponent(channelB);
    if (!scA || !scB)
        return false;
    const dA = scA.getWithExplicit('domains');
    const dB = scB.getWithExplicit('domains');
    if (!(dA?.explicit && dB?.explicit))
        return false;
    const assembledA = assembleDomain(model, channelA);
    const assembledB = assembleDomain(model, channelB);
    return hash(assembledA) === hash(assembledB);
}
/**
 * Assemble legends for a model. We group legends by the underlying field used by the encoding.
 *
 * @param model - The model to assemble legends for
 * @returns The assembled legends
 */
function assembleLegends(model) {
    const legendComponentIndex = model.component.legends;
    const legendsByGroup = {};
    for (const channel of keys(legendComponentIndex)) {
        const fieldKey = getFieldKeyForChannel(model, channel);
        const groupKey = getLegendGroupKey(fieldKey, channel);
        if (!legendsByGroup[groupKey]) {
            legendsByGroup[groupKey] = [{ channel, cmpt: legendComponentIndex[channel].clone() }];
            continue;
        }
        let mergedIntoExisting = false;
        for (const existing of legendsByGroup[groupKey]) {
            if (!legendsAreMergeCompatible(model, existing.channel, channel)) {
                continue;
            }
            const merged = mergeLegendComponent(existing.cmpt, legendComponentIndex[channel]);
            if (merged) {
                const typeA = model.getScaleType(existing.channel);
                const typeB = model.getScaleType(channel);
                if (typeA && typeB && hasDiscreteDomain(typeA) && hasDiscreteDomain(typeB)) {
                    if (domainsExplicitAndEqual(model, existing.channel, channel)) {
                        setImplicitLegendValues(existing.cmpt, getDiscreteValuesForChannel(model, existing.channel));
                    }
                    else {
                        setImplicitLegendValues(existing.cmpt, unionDiscreteValuesForChannels(model, existing.channel, channel), 
                        // Warn when unioning discrete legend values so that users are aware
                        legendValuesUnioned(existing.channel, channel));
                    }
                }
                mergedIntoExisting = true;
                break;
            }
        }
        if (!mergedIntoExisting) {
            legendsByGroup[groupKey].push({ channel, cmpt: legendComponentIndex[channel].clone() });
        }
    }
    const legends = vals(legendsByGroup)
        .flat()
        .map((entry) => assembleLegend(entry.cmpt, model.config))
        .filter((l) => l !== undefined);
    return legends;
}
function assembleLegend(legendCmpt, config) {
    const { disable, labelExpr, selections, ...legend } = legendCmpt.combine();
    if (disable) {
        return undefined;
    }
    if (config.aria === false && legend.aria == undefined) {
        legend.aria = false;
    }
    if (legend.encode?.symbols) {
        const out = legend.encode.symbols.update;
        if (out.fill && out.fill['value'] !== 'transparent' && !out.stroke && !legend.stroke) {
            // For non color channel's legend, we need to override symbol stroke config from Vega config if stroke channel is not used.
            out.stroke = { value: 'transparent' };
        }
        // Remove properties that the legend is encoding.
        for (const property of LEGEND_SCALE_CHANNELS) {
            if (legend[property]) {
                delete out[property];
            }
        }
    }
    if (!legend.title) {
        // title schema doesn't include null, ''
        delete legend.title;
    }
    if (labelExpr !== undefined) {
        let expr = labelExpr;
        if (legend.encode?.labels?.update && isSignalRef(legend.encode.labels.update.text)) {
            expr = replaceAll(labelExpr, 'datum.label', legend.encode.labels.update.text.signal);
        }
        setLegendEncode(legend, 'labels', 'text', { signal: expr });
    }
    return legend;
}

function assembleProjections(model) {
    if (isLayerModel(model) || isConcatModel(model)) {
        return assembleProjectionsForModelAndChildren(model);
    }
    else {
        return assembleProjectionForModel(model);
    }
}
function assembleProjectionsForModelAndChildren(model) {
    return model.children.reduce((projections, child) => {
        return projections.concat(child.assembleProjections());
    }, assembleProjectionForModel(model));
}
function assembleProjectionForModel(model) {
    const component = model.component.projection;
    if (!component || component.merged) {
        return [];
    }
    const projection = component.combine();
    const { name } = projection; // we need to extract name so that it is always present in the output and pass TS type validation
    if (!component.data) {
        // generate custom projection, no automatic fitting
        return [
            {
                name,
                // translate to center by default
                translate: { signal: '[width / 2, height / 2]' },
                // parameters, overwrite default translate if specified
                ...projection,
            },
        ];
    }
    else {
        // generate projection that uses extent fitting
        const size = {
            signal: `[${component.size.map((ref) => ref.signal).join(', ')}]`,
        };
        const fits = component.data.reduce((sources, data) => {
            const source = isSignalRef(data) ? data.signal : `data('${model.lookupDataSource(data)}')`;
            if (!contains(sources, source)) {
                // build a unique list of sources
                sources.push(source);
            }
            return sources;
        }, []);
        if (fits.length <= 0) {
            throw new Error("Projection's fit didn't find any data sources");
        }
        return [
            {
                name,
                size,
                fit: {
                    signal: fits.length > 1 ? `[${fits.join(', ')}]` : fits[0],
                },
                ...projection,
            },
        ];
    }
}

const PROJECTION_PROPERTIES = [
    'type',
    'clipAngle',
    'clipExtent',
    'center',
    'rotate',
    'precision',
    'reflectX',
    'reflectY',
    'coefficient',
    'distance',
    'fraction',
    'lobes',
    'parallel',
    'radius',
    'ratio',
    'spacing',
    'tilt',
];

class ProjectionComponent extends Split {
    specifiedProjection;
    size;
    data;
    merged = false;
    constructor(name, specifiedProjection, size, data) {
        super({ ...specifiedProjection }, // all explicit properties of projection
        { name });
        this.specifiedProjection = specifiedProjection;
        this.size = size;
        this.data = data;
    }
    /**
     * Whether the projection parameters should fit provided data.
     */
    get isFit() {
        return !!this.data;
    }
}

function parseProjection(model) {
    model.component.projection = isUnitModel(model) ? parseUnitProjection(model) : parseNonUnitProjections(model);
}
function parseUnitProjection(model) {
    if (model.hasProjection) {
        const proj = replaceExprRef(model.specifiedProjection);
        const fit = !(proj && (proj.scale != null || proj.translate != null));
        const size = fit ? [model.getSizeSignalRef('width'), model.getSizeSignalRef('height')] : undefined;
        const data = fit ? gatherFitData(model) : undefined;
        const projComp = new ProjectionComponent(model.projectionName(true), {
            ...replaceExprRef(model.config.projection),
            ...proj,
        }, size, data);
        if (!projComp.get('type')) {
            projComp.set('type', 'equalEarth', false);
        }
        return projComp;
    }
    return undefined;
}
function gatherFitData(model) {
    const data = [];
    const { encoding } = model;
    for (const posssiblePair of [
        [LONGITUDE, LATITUDE],
        [LONGITUDE2, LATITUDE2],
    ]) {
        if (getFieldOrDatumDef(encoding[posssiblePair[0]]) || getFieldOrDatumDef(encoding[posssiblePair[1]])) {
            data.push({
                signal: model.getName(`geojson_${data.length}`),
            });
        }
    }
    if (model.channelHasField(SHAPE) && model.typedFieldDef(SHAPE).type === GEOJSON) {
        data.push({
            signal: model.getName(`geojson_${data.length}`),
        });
    }
    if (data.length === 0) {
        // main source is geojson, so we can just use that
        data.push(model.requestDataName(DataSourceType.Main));
    }
    return data;
}
function mergeIfNoConflict(first, second) {
    const allPropertiesShared = every(PROJECTION_PROPERTIES, (prop) => {
        // neither has the property
        if (!has$1(first.explicit, prop) && !has$1(second.explicit, prop)) {
            return true;
        }
        // both have property and an equal value for property
        if (has$1(first.explicit, prop) &&
            has$1(second.explicit, prop) &&
            // some properties might be signals or objects and require hashing for comparison
            deepEqual(first.get(prop), second.get(prop))) {
            return true;
        }
        return false;
    });
    const size = deepEqual(first.size, second.size);
    if (size) {
        if (allPropertiesShared) {
            return first;
        }
        else if (deepEqual(first.explicit, {})) {
            return second;
        }
        else if (deepEqual(second.explicit, {})) {
            return first;
        }
    }
    // if all properties don't match, let each unit spec have its own projection
    return null;
}
function parseNonUnitProjections(model) {
    if (model.children.length === 0) {
        return undefined;
    }
    let nonUnitProjection;
    // parse all children first
    for (const child of model.children) {
        parseProjection(child);
    }
    // analyze parsed projections, attempt to merge
    const mergable = every(model.children, (child) => {
        const projection = child.component.projection;
        if (!projection) {
            // child layer does not use a projection
            return true;
        }
        else if (!nonUnitProjection) {
            // cached 'projection' is null, cache this one
            nonUnitProjection = projection;
            return true;
        }
        else {
            const merge = mergeIfNoConflict(nonUnitProjection, projection);
            if (merge) {
                nonUnitProjection = merge;
            }
            return !!merge;
        }
    });
    // if cached one and all other children share the same projection,
    if (nonUnitProjection && mergable) {
        // so we can elevate it to the layer level
        const name = model.projectionName(true);
        const modelProjection = new ProjectionComponent(name, nonUnitProjection.specifiedProjection, nonUnitProjection.size, duplicate(nonUnitProjection.data));
        // rename and assign all others as merged
        for (const child of model.children) {
            const projection = child.component.projection;
            if (projection) {
                if (projection.isFit) {
                    modelProjection.data.push(...child.component.projection.data);
                }
                child.renameProjection(projection.get('name'), name);
                projection.merged = true;
            }
        }
        return modelProjection;
    }
    return undefined;
}

function assembleScales(model) {
    if (isLayerModel(model) || isConcatModel(model)) {
        // For concat and layer, include scales of children too
        return model.children.reduce((scales, child) => {
            return scales.concat(assembleScales(child));
        }, assembleScalesForModel(model));
    }
    else {
        // For facet, child scales would not be included in the parent's scope.
        // For unit, there is no child.
        return assembleScalesForModel(model);
    }
}
function assembleScalesForModel(model) {
    return keys(model.component.scales).reduce((scales, channel) => {
        const scaleComponent = model.component.scales[channel];
        if (scaleComponent.merged) {
            // Skipped merged scales
            return scales;
        }
        const scale = scaleComponent.combine();
        const { name, type, selectionExtent, domains: _d, range: _r, reverse, ...otherScaleProps } = scale;
        const range = assembleScaleRange(scale.range, name, channel, model);
        const domain = assembleDomain(model, channel);
        const domainRaw = selectionExtent
            ? assembleSelectionScaleDomain(model, selectionExtent, scaleComponent, domain)
            : null;
        scales.push({
            name,
            type,
            ...(domain ? { domain } : {}),
            ...(domainRaw ? { domainRaw } : {}),
            range,
            ...(reverse !== undefined ? { reverse: reverse } : {}),
            ...otherScaleProps,
        });
        return scales;
    }, []);
}
function assembleScaleRange(scaleRange, scaleName, channel, model) {
    // add signals to x/y range
    if (isXorY(channel)) {
        if (isVgRangeStep(scaleRange)) {
            // For width/height step, use a signal created in layout assemble instead of a constant step.
            return {
                step: { signal: `${scaleName}_step` },
            };
        }
    }
    else if (isObject(scaleRange) && isDataRefDomain(scaleRange)) {
        return {
            ...scaleRange,
            data: model.lookupDataSource(scaleRange.data),
        };
    }
    return scaleRange;
}

class ScaleComponent extends Split {
    merged = false;
    constructor(name, typeWithExplicit) {
        super({}, // no initial explicit property
        { name });
        this.setWithExplicit('type', typeWithExplicit);
    }
    /**
     * Whether the scale definitely includes or not include zero in the domain
     */
    domainHasZero() {
        const scaleType = this.get('type');
        if (contains([ScaleType.LOG, ScaleType.TIME, ScaleType.UTC], scaleType)) {
            // Log scales cannot have zero.
            // Zero in time scale is arbitrary, and does not affect ratio.
            // (Time is an interval level of measurement, not ratio).
            // See https://en.wikipedia.org/wiki/Level_of_measurement for more info.
            return 'definitely-not';
        }
        const scaleZero = this.get('zero');
        if (scaleZero === true ||
            // If zero is undefined, linear/sqrt/pow scales have zero by default.
            (scaleZero === undefined && contains([ScaleType.LINEAR, ScaleType.SQRT, ScaleType.POW], scaleType))) {
            return 'definitely';
        }
        const domains = this.get('domains');
        if (domains.length > 0) {
            let hasExplicitDomainWithZero = false;
            let hasExplicitDomainWithoutZero = false;
            let hasDomainBasedOnField = false;
            for (const d of domains) {
                if (isArray(d)) {
                    const first = d[0];
                    const last = d[d.length - 1];
                    if (isNumber$1(first) && isNumber$1(last)) {
                        if (first <= 0 && last >= 0) {
                            hasExplicitDomainWithZero = true;
                            continue;
                        }
                        else {
                            hasExplicitDomainWithoutZero = true;
                            continue;
                        }
                    }
                }
                hasDomainBasedOnField = true;
            }
            if (hasExplicitDomainWithZero) {
                return 'definitely';
            }
            else if (hasExplicitDomainWithoutZero && !hasDomainBasedOnField) {
                return 'definitely-not';
            }
        }
        return 'maybe';
    }
}

const RANGE_PROPERTIES = ['range', 'scheme'];
function parseUnitScaleRange(model) {
    const localScaleComponents = model.component.scales;
    // use SCALE_CHANNELS instead of scales[channel] to ensure that x, y come first!
    for (const channel of SCALE_CHANNELS) {
        const localScaleCmpt = localScaleComponents[channel];
        if (!localScaleCmpt) {
            continue;
        }
        const rangeWithExplicit = parseRangeForChannel(channel, model);
        localScaleCmpt.setWithExplicit('range', rangeWithExplicit);
    }
}
function getBinStepSignal(model, channel) {
    const fieldDef = model.fieldDef(channel);
    if (fieldDef?.bin) {
        const { bin, field } = fieldDef;
        const sizeType = getSizeChannel(channel);
        const sizeSignal = model.getName(sizeType);
        if (isObject(bin) && bin.binned && bin.step !== undefined) {
            return new SignalRefWrapper(() => {
                const scaleName = model.scaleName(channel);
                const binCount = `(domain("${scaleName}")[1] - domain("${scaleName}")[0]) / ${bin.step}`;
                return `${model.getSignalName(sizeSignal)} / (${binCount})`;
            });
        }
        else if (isBinning(bin)) {
            const binSignal = getBinSignalName(model, field, bin);
            // TODO: extract this to be range step signal
            return new SignalRefWrapper(() => {
                const updatedName = model.getSignalName(binSignal);
                const binCount = `(${updatedName}.stop - ${updatedName}.start) / ${updatedName}.step`;
                return `${model.getSignalName(sizeSignal)} / (${binCount})`;
            });
        }
    }
    return undefined;
}
/**
 * Return mixins that includes one of the Vega range types (explicit range, range.step, range.scheme).
 */
function parseRangeForChannel(channel, model) {
    const specifiedScale = model.specifiedScales[channel];
    const { size } = model;
    const mergedScaleCmpt = model.getScaleComponent(channel);
    const scaleType = mergedScaleCmpt.get('type');
    // Check if any of the range properties is specified.
    // If so, check if it is compatible and make sure that we only output one of the properties
    for (const property of RANGE_PROPERTIES) {
        if (specifiedScale[property] !== undefined) {
            const supportedByScaleType = scaleTypeSupportProperty(scaleType, property);
            const channelIncompatability = channelScalePropertyIncompatability(channel, property);
            if (!supportedByScaleType) {
                warn(scalePropertyNotWorkWithScaleType(scaleType, property, channel));
            }
            else if (channelIncompatability) {
                // channel
                warn(channelIncompatability);
            }
            else {
                switch (property) {
                    case 'range': {
                        const range = specifiedScale.range;
                        if (isArray(range)) {
                            if (isXorY(channel)) {
                                return makeExplicit(range.map((v) => {
                                    if (v === 'width' || v === 'height') {
                                        // get signal for width/height
                                        // Just like default range logic below, we use SignalRefWrapper to account for potential merges and renames.
                                        const sizeSignal = model.getName(v);
                                        const getSignalName = model.getSignalName.bind(model);
                                        return SignalRefWrapper.fromName(getSignalName, sizeSignal);
                                    }
                                    return v;
                                }));
                            }
                        }
                        else if (isObject(range)) {
                            return makeExplicit({
                                data: model.requestDataName(DataSourceType.Main),
                                field: range.field,
                                sort: { op: 'min', field: model.vgField(channel) },
                            });
                        }
                        return makeExplicit(range);
                    }
                    case 'scheme':
                        return makeExplicit(parseScheme(specifiedScale[property]));
                }
            }
        }
    }
    const sizeChannel = channel === X || channel === 'xOffset' ? 'width' : 'height';
    const sizeValue = size[sizeChannel];
    if (isStep(sizeValue)) {
        if (isXorY(channel)) {
            if (hasDiscreteDomain(scaleType)) {
                const step = getPositionStep(sizeValue, model, channel);
                // Need to be explicit so layer with step wins over layer without step
                if (step) {
                    return makeExplicit({ step });
                }
            }
            else {
                warn(stepDropped(sizeChannel));
            }
        }
        else if (isXorYOffset(channel)) {
            const positionChannel = channel === XOFFSET ? 'x' : 'y';
            const positionScaleCmpt = model.getScaleComponent(positionChannel);
            const positionScaleType = positionScaleCmpt.get('type');
            if (positionScaleType === 'band') {
                const step = getOffsetStep(sizeValue, scaleType);
                if (step) {
                    return makeExplicit(step);
                }
            }
        }
    }
    const { rangeMin, rangeMax } = specifiedScale;
    const d = defaultRange(channel, model);
    if ((rangeMin !== undefined || rangeMax !== undefined) &&
        // it's ok to check just rangeMin's compatibility since rangeMin/rangeMax are the same
        scaleTypeSupportProperty(scaleType, 'rangeMin') &&
        isArray(d) &&
        d.length === 2) {
        return makeExplicit([rangeMin ?? d[0], rangeMax ?? d[1]]);
    }
    return makeImplicit(d);
}
function parseScheme(scheme) {
    if (isExtendedScheme(scheme)) {
        return {
            scheme: scheme.name,
            ...omit(scheme, ['name']),
        };
    }
    return { scheme };
}
function fullWidthOrHeightRange(channel, model, scaleType, { center } = {}) {
    // If step is null, use zero to width or height.
    // Note that we use SignalRefWrapper to account for potential merges and renames.
    const sizeType = getSizeChannel(channel);
    const sizeSignal = model.getName(sizeType);
    const getSignalName = model.getSignalName.bind(model);
    if (channel === Y && hasContinuousDomain(scaleType)) {
        // For y continuous scale, we have to start from the height as the bottom part has the max value.
        return center
            ? [
                SignalRefWrapper.fromName((name) => `${getSignalName(name)}/2`, sizeSignal),
                SignalRefWrapper.fromName((name) => `-${getSignalName(name)}/2`, sizeSignal),
            ]
            : [SignalRefWrapper.fromName(getSignalName, sizeSignal), 0];
    }
    else {
        return center
            ? [
                SignalRefWrapper.fromName((name) => `-${getSignalName(name)}/2`, sizeSignal),
                SignalRefWrapper.fromName((name) => `${getSignalName(name)}/2`, sizeSignal),
            ]
            : [0, SignalRefWrapper.fromName(getSignalName, sizeSignal)];
    }
}
function defaultRange(channel, model) {
    const { size, config, mark, encoding } = model;
    const { type } = getFieldOrDatumDef(encoding[channel]);
    const mergedScaleCmpt = model.getScaleComponent(channel);
    const scaleType = mergedScaleCmpt.get('type');
    const { domain, domainMid } = model.specifiedScales[channel];
    switch (channel) {
        case X:
        case Y: {
            // If there is no explicit width/height for discrete x/y scales
            if (contains(['point', 'band'], scaleType)) {
                const positionSize = getDiscretePositionSize(channel, size, config.view);
                if (isStep(positionSize)) {
                    const step = getPositionStep(positionSize, model, channel);
                    return { step };
                }
            }
            return fullWidthOrHeightRange(channel, model, scaleType);
        }
        case XOFFSET:
        case YOFFSET:
            return getOffsetRange(channel, model, scaleType);
        case SIZE: {
            // TODO: support custom rangeMin, rangeMax
            const rangeMin = sizeRangeMin(mark, config);
            const rangeMax = sizeRangeMax(mark, size, model, config);
            if (isContinuousToDiscrete(scaleType)) {
                return interpolateRange(rangeMin, rangeMax, defaultContinuousToDiscreteCount(scaleType, config, domain, channel));
            }
            else {
                return [rangeMin, rangeMax];
            }
        }
        case THETA:
            return [0, Math.PI * 2];
        case ANGLE:
            // TODO: add config.scale.min/maxAngleDegree (for point and text) and config.scale.min/maxAngleRadian (for arc) once we add arc marks.
            // (It's weird to add just config.scale.min/maxAngleDegree for now)
            return [0, 360];
        case RADIUS: {
            // max radius = half od min(width,height)
            return [
                0,
                new SignalRefWrapper(() => {
                    const w = model.getSignalName(isFacetModel(model.parent) ? 'child_width' : 'width');
                    const h = model.getSignalName(isFacetModel(model.parent) ? 'child_height' : 'height');
                    return `min(${w},${h})/2`;
                }),
            ];
        }
        case TIME: {
            // if (scaleType === 'band') {
            return { step: 1000 / config.scale.framesPerSecond };
            // }
            // return [0, config.scale.animationDuration * 1000]; // TODO(jzong): uncomment for linear scales when interpolation is implemented
        }
        case STROKEWIDTH:
            // TODO: support custom rangeMin, rangeMax
            return [config.scale.minStrokeWidth, config.scale.maxStrokeWidth];
        case STROKEDASH:
            return [
                // TODO: add this to Vega's config.range?
                [1, 0],
                [4, 2],
                [2, 1],
                [1, 1],
                [1, 2, 4, 2],
            ];
        case SHAPE:
            return 'symbol';
        case COLOR:
        case FILL:
        case STROKE:
            if (scaleType === 'ordinal') {
                // Only nominal data uses ordinal scale by default
                return type === 'nominal' ? 'category' : 'ordinal';
            }
            else {
                if (domainMid !== undefined) {
                    return 'diverging';
                }
                else {
                    return mark === 'rect' || mark === 'geoshape' ? 'heatmap' : 'ramp';
                }
            }
        case OPACITY:
        case FILLOPACITY:
        case STROKEOPACITY:
            // TODO: support custom rangeMin, rangeMax
            return [config.scale.minOpacity, config.scale.maxOpacity];
    }
}
function getPositionStep(step, model, channel) {
    const { encoding } = model;
    const mergedScaleCmpt = model.getScaleComponent(channel);
    const offsetChannel = getOffsetScaleChannel(channel);
    const offsetDef = encoding[offsetChannel];
    const stepFor = getStepFor({ step, offsetIsDiscrete: isFieldOrDatumDef(offsetDef) && isDiscrete$1(offsetDef.type) });
    if (stepFor === 'offset' && channelHasFieldOrDatum(encoding, offsetChannel)) {
        const offsetScaleCmpt = model.getScaleComponent(offsetChannel);
        const offsetScaleName = model.scaleName(offsetChannel);
        let stepCount = `domain('${offsetScaleName}').length`;
        if (offsetScaleCmpt.get('type') === 'band') {
            const offsetPaddingInner = offsetScaleCmpt.get('paddingInner') ?? offsetScaleCmpt.get('padding') ?? 0;
            const offsetPaddingOuter = offsetScaleCmpt.get('paddingOuter') ?? offsetScaleCmpt.get('padding') ?? 0;
            stepCount = `bandspace(${stepCount}, ${offsetPaddingInner}, ${offsetPaddingOuter})`;
        }
        const paddingInner = mergedScaleCmpt.get('paddingInner') ?? mergedScaleCmpt.get('padding');
        return {
            signal: `${step.step} * ${stepCount} / (1-${exprFromSignalRefOrValue(paddingInner)})`,
        };
    }
    else {
        return step.step;
    }
}
function getOffsetStep(step, offsetScaleType) {
    const stepFor = getStepFor({ step, offsetIsDiscrete: hasDiscreteDomain(offsetScaleType) });
    if (stepFor === 'offset') {
        return { step: step.step };
    }
    return undefined;
}
function getOffsetRange(channel, model, offsetScaleType) {
    const positionChannel = channel === XOFFSET ? 'x' : 'y';
    const positionScaleCmpt = model.getScaleComponent(positionChannel);
    if (!positionScaleCmpt) {
        return fullWidthOrHeightRange(positionChannel, model, offsetScaleType, { center: true });
    }
    const positionScaleType = positionScaleCmpt.get('type');
    const positionScaleName = model.scaleName(positionChannel);
    const { markDef, config } = model;
    if (positionScaleType === 'band') {
        const size = getDiscretePositionSize(positionChannel, model.size, model.config.view);
        if (isStep(size)) {
            // step is for offset
            const step = getOffsetStep(size, offsetScaleType);
            if (step) {
                return step;
            }
        }
        // otherwise use the position
        return [0, { signal: `bandwidth('${positionScaleName}')` }];
    }
    else {
        // continuous scale
        const positionDef = model.encoding[positionChannel];
        if (isFieldDef(positionDef) && positionDef.timeUnit) {
            const duration = durationExpr(positionDef.timeUnit, (expr) => `scale('${positionScaleName}', ${expr})`);
            const padding = model.config.scale.bandWithNestedOffsetPaddingInner;
            const bandPositionOffset = getBandPosition({
                fieldDef: positionDef,
                markDef,
                config,
            }) - 0.5;
            const bandPositionOffsetExpr = bandPositionOffset !== 0 ? ` + ${bandPositionOffset}` : '';
            if (padding) {
                const startRatio = isSignalRef(padding)
                    ? `${padding.signal}/2${bandPositionOffsetExpr}`
                    : `${padding / 2 + bandPositionOffset}`;
                const endRatio = isSignalRef(padding)
                    ? `(1 - ${padding.signal}/2)${bandPositionOffsetExpr}`
                    : `${1 - padding / 2 + bandPositionOffset}`;
                return [{ signal: `${startRatio} * (${duration})` }, { signal: `${endRatio} * (${duration})` }];
            }
            return [0, { signal: duration }];
        }
        return never(`Cannot use ${channel} scale if ${positionChannel} scale is not discrete.`);
    }
}
function getDiscretePositionSize(channel, size, viewConfig) {
    const sizeChannel = channel === X ? 'width' : 'height';
    const sizeValue = size[sizeChannel];
    if (sizeValue !== undefined) {
        return sizeValue;
    }
    return getViewConfigDiscreteSize(viewConfig, sizeChannel);
}
function defaultContinuousToDiscreteCount(scaleType, config, domain, channel) {
    switch (scaleType) {
        case 'quantile':
            return config.scale.quantileCount;
        case 'quantize':
            return config.scale.quantizeCount;
        case 'threshold':
            if (domain !== undefined && isArray(domain)) {
                return domain.length + 1;
            }
            else {
                warn(domainRequiredForThresholdScale(channel));
                // default threshold boundaries for threshold scale since domain has cardinality of 2
                return 3;
            }
    }
}
/**
 * Returns the linear interpolation of the range according to the cardinality
 *
 * @param rangeMin start of the range
 * @param rangeMax end of the range
 * @param cardinality number of values in the output range
 */
function interpolateRange(rangeMin, rangeMax, cardinality) {
    // always return a signal since it's better to compute the sequence in Vega later
    const f = () => {
        const rMax = signalOrStringValue(rangeMax);
        const rMin = signalOrStringValue(rangeMin);
        const step = `(${rMax} - ${rMin}) / (${cardinality} - 1)`;
        return `sequence(${rMin}, ${rMax} + ${step}, ${step})`;
    };
    if (isSignalRef(rangeMax)) {
        return new SignalRefWrapper(f);
    }
    else {
        return { signal: f() };
    }
}
function sizeRangeMin(mark, config) {
    switch (mark) {
        case 'bar':
        case 'tick':
            return config.scale.minBandSize;
        case 'line':
        case 'trail':
        case 'rule':
            return config.scale.minStrokeWidth;
        case 'text':
            return config.scale.minFontSize;
        case 'point':
        case 'square':
        case 'circle':
            return config.scale.minSize;
    }
    /* istanbul ignore next: should never reach here */
    // sizeRangeMin not implemented for the mark
    throw new Error(incompatibleChannel('size', mark));
}
const MAX_SIZE_RANGE_STEP_RATIO = 0.95;
function sizeRangeMax(mark, size, model, config) {
    const xyStepSignals = {
        x: getBinStepSignal(model, 'x'),
        y: getBinStepSignal(model, 'y'),
    };
    switch (mark) {
        case 'bar':
        case 'tick': {
            if (config.scale.maxBandSize !== undefined) {
                return config.scale.maxBandSize;
            }
            const min = minXYStep(size, xyStepSignals, config.view);
            if (isNumber$1(min)) {
                return min - 1;
            }
            else {
                return new SignalRefWrapper(() => `${min.signal} - 1`);
            }
        }
        case 'line':
        case 'trail':
        case 'rule':
            return config.scale.maxStrokeWidth;
        case 'text':
            return config.scale.maxFontSize;
        case 'point':
        case 'square':
        case 'circle': {
            if (config.scale.maxSize) {
                return config.scale.maxSize;
            }
            const pointStep = minXYStep(size, xyStepSignals, config.view);
            if (isNumber$1(pointStep)) {
                return Math.pow(MAX_SIZE_RANGE_STEP_RATIO * pointStep, 2);
            }
            else {
                return new SignalRefWrapper(() => `pow(${MAX_SIZE_RANGE_STEP_RATIO} * ${pointStep.signal}, 2)`);
            }
        }
    }
    /* istanbul ignore next: should never reach here */
    // sizeRangeMax not implemented for the mark
    throw new Error(incompatibleChannel('size', mark));
}
/**
 * @returns {number} Range step of x or y or minimum between the two if both are ordinal scale.
 */
function minXYStep(size, xyStepSignals, viewConfig) {
    const widthStep = isStep(size.width) ? size.width.step : getViewConfigDiscreteStep(viewConfig, 'width');
    const heightStep = isStep(size.height) ? size.height.step : getViewConfigDiscreteStep(viewConfig, 'height');
    if (xyStepSignals.x || xyStepSignals.y) {
        return new SignalRefWrapper(() => {
            const exprs = [
                xyStepSignals.x ? xyStepSignals.x.signal : widthStep,
                xyStepSignals.y ? xyStepSignals.y.signal : heightStep,
            ];
            return `min(${exprs.join(', ')})`;
        });
    }
    return Math.min(widthStep, heightStep);
}

function parseScaleProperty(model, property) {
    if (isUnitModel(model)) {
        parseUnitScaleProperty(model, property);
    }
    else {
        parseNonUnitScaleProperty(model, property);
    }
}
function parseUnitScaleProperty(model, property) {
    const localScaleComponents = model.component.scales;
    const { config, encoding, markDef, specifiedScales } = model;
    for (const channel of keys(localScaleComponents)) {
        const specifiedScale = specifiedScales[channel];
        const localScaleCmpt = localScaleComponents[channel];
        const mergedScaleCmpt = model.getScaleComponent(channel);
        const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]);
        const specifiedValue = specifiedScale[property];
        const scaleType = mergedScaleCmpt.get('type');
        const scalePadding = mergedScaleCmpt.get('padding');
        const scalePaddingInner = mergedScaleCmpt.get('paddingInner');
        const supportedByScaleType = scaleTypeSupportProperty(scaleType, property);
        const channelIncompatability = channelScalePropertyIncompatability(channel, property);
        if (specifiedValue !== undefined) {
            // If there is a specified value, check if it is compatible with scale type and channel
            if (!supportedByScaleType) {
                warn(scalePropertyNotWorkWithScaleType(scaleType, property, channel));
            }
            else if (channelIncompatability) {
                // channel
                warn(channelIncompatability);
            }
        }
        if (supportedByScaleType && channelIncompatability === undefined) {
            if (specifiedValue !== undefined) {
                const timeUnit = fieldOrDatumDef.timeUnit;
                const type = fieldOrDatumDef.type;
                switch (property) {
                    // domainMax/Min to signal if the value is a datetime object
                    case 'domainMax':
                    case 'domainMin':
                        if (isDateTime(specifiedScale[property]) || type === 'temporal' || timeUnit) {
                            localScaleCmpt.set(property, { signal: valueExpr(specifiedScale[property], { type, timeUnit }) }, true);
                        }
                        else {
                            localScaleCmpt.set(property, specifiedScale[property], true);
                        }
                        break;
                    default:
                        localScaleCmpt.copyKeyFromObject(property, specifiedScale);
                }
            }
            else {
                const value = hasProperty(scaleRules, property)
                    ? scaleRules[property]({
                        model,
                        channel,
                        fieldOrDatumDef,
                        scaleType,
                        scalePadding,
                        scalePaddingInner,
                        domain: specifiedScale.domain,
                        domainMin: specifiedScale.domainMin,
                        domainMax: specifiedScale.domainMax,
                        markDef,
                        config,
                        hasNestedOffsetScale: channelHasNestedOffsetScale(encoding, channel),
                        hasSecondaryRangeChannel: !!encoding[getSecondaryRangeChannel(channel)],
                    })
                    : config.scale[property];
                if (value !== undefined) {
                    localScaleCmpt.set(property, value, false);
                }
            }
        }
    }
}
const scaleRules = {
    bins: ({ model, fieldOrDatumDef }) => (isFieldDef(fieldOrDatumDef) ? bins(model, fieldOrDatumDef) : undefined),
    interpolate: ({ channel, fieldOrDatumDef }) => interpolate(channel, fieldOrDatumDef.type),
    nice: ({ scaleType, channel, domain, domainMin, domainMax, fieldOrDatumDef }) => nice(scaleType, channel, domain, domainMin, domainMax, fieldOrDatumDef),
    padding: ({ channel, scaleType, fieldOrDatumDef, markDef, config }) => padding(channel, scaleType, config.scale, fieldOrDatumDef, markDef, config.bar),
    paddingInner: ({ scalePadding, channel, markDef, scaleType, config, hasNestedOffsetScale }) => paddingInner(scalePadding, channel, markDef.type, scaleType, config.scale, hasNestedOffsetScale),
    paddingOuter: ({ scalePadding, channel, scaleType, scalePaddingInner, config, hasNestedOffsetScale }) => paddingOuter(scalePadding, channel, scaleType, scalePaddingInner, config.scale, hasNestedOffsetScale),
    reverse: ({ fieldOrDatumDef, scaleType, channel, config }) => {
        const sort = isFieldDef(fieldOrDatumDef) ? fieldOrDatumDef.sort : undefined;
        return reverse(scaleType, sort, channel, config.scale);
    },
    zero: ({ channel, fieldOrDatumDef, domain, markDef, scaleType, config, hasSecondaryRangeChannel }) => zero(channel, fieldOrDatumDef, domain, markDef, scaleType, config.scale, hasSecondaryRangeChannel),
};
// This method is here rather than in range.ts to avoid circular dependency.
function parseScaleRange(model) {
    if (isUnitModel(model)) {
        parseUnitScaleRange(model);
    }
    else {
        parseNonUnitScaleProperty(model, 'range');
    }
}
function parseNonUnitScaleProperty(model, property) {
    const localScaleComponents = model.component.scales;
    for (const child of model.children) {
        if (property === 'range') {
            parseScaleRange(child);
        }
        else {
            parseScaleProperty(child, property);
        }
    }
    for (const channel of keys(localScaleComponents)) {
        let valueWithExplicit;
        for (const child of model.children) {
            const childComponent = child.component.scales[channel];
            if (childComponent) {
                const childValueWithExplicit = childComponent.getWithExplicit(property);
                valueWithExplicit = mergeValuesWithExplicit(valueWithExplicit, childValueWithExplicit, property, 'scale', tieBreakByComparing((v1, v2) => {
                    switch (property) {
                        case 'range':
                            // For step, prefer larger step
                            if (v1.step && v2.step) {
                                return v1.step - v2.step;
                            }
                            return 0;
                        // TODO: precedence rule for other properties
                    }
                    return 0;
                }));
            }
        }
        localScaleComponents[channel].setWithExplicit(property, valueWithExplicit);
    }
}
function bins(model, fieldDef) {
    const bin = fieldDef.bin;
    if (isBinning(bin)) {
        const binSignal = getBinSignalName(model, fieldDef.field, bin);
        return new SignalRefWrapper(() => {
            return model.getSignalName(binSignal);
        });
    }
    else if (isBinned(bin) && isBinParams(bin) && bin.step !== undefined) {
        // start and stop will be determined from the scale domain
        return {
            step: bin.step,
        };
    }
    return undefined;
}
function interpolate(channel, type) {
    if (contains([COLOR, FILL, STROKE], channel) && type !== 'nominal') {
        return 'hcl';
    }
    return undefined;
}
function nice(scaleType, channel, specifiedDomain, domainMin, domainMax, fieldOrDatumDef) {
    if (getFieldDef(fieldOrDatumDef)?.bin ||
        isArray(specifiedDomain) ||
        domainMax != null ||
        domainMin != null ||
        contains([ScaleType.TIME, ScaleType.UTC], scaleType)) {
        return undefined;
    }
    return isXorY(channel) ? true : undefined;
}
function padding(channel, scaleType, scaleConfig, fieldOrDatumDef, markDef, barConfig) {
    if (isXorY(channel)) {
        if (isContinuousToContinuous(scaleType)) {
            if (scaleConfig.continuousPadding !== undefined) {
                return scaleConfig.continuousPadding;
            }
            const { type, orient } = markDef;
            if (type === 'bar' && !(isFieldDef(fieldOrDatumDef) && (fieldOrDatumDef.bin || fieldOrDatumDef.timeUnit))) {
                if ((orient === 'vertical' && channel === 'x') || (orient === 'horizontal' && channel === 'y')) {
                    return barConfig.continuousBandSize;
                }
            }
        }
        if (scaleType === ScaleType.POINT) {
            return scaleConfig.pointPadding;
        }
    }
    return undefined;
}
function paddingInner(paddingValue, channel, mark, scaleType, scaleConfig, hasNestedOffsetScale = false) {
    if (paddingValue !== undefined) {
        // If user has already manually specified "padding", no need to add default paddingInner.
        return undefined;
    }
    if (isXorY(channel)) {
        // Padding is only set for X and Y by default.
        // Basically it doesn't make sense to add padding for color and size.
        // paddingOuter would only be called if it's a band scale, just return the default for bandScale.
        const { bandPaddingInner, barBandPaddingInner, rectBandPaddingInner, tickBandPaddingInner, bandWithNestedOffsetPaddingInner, } = scaleConfig;
        if (hasNestedOffsetScale) {
            return bandWithNestedOffsetPaddingInner;
        }
        return getFirstDefined(bandPaddingInner, mark === 'bar' ? barBandPaddingInner : mark === 'tick' ? tickBandPaddingInner : rectBandPaddingInner);
    }
    else if (isXorYOffset(channel)) {
        if (scaleType === ScaleType.BAND) {
            return scaleConfig.offsetBandPaddingInner;
        }
    }
    return undefined;
}
function paddingOuter(paddingValue, channel, scaleType, paddingInnerValue, scaleConfig, hasNestedOffsetScale = false) {
    if (paddingValue !== undefined) {
        // If user has already manually specified "padding", no need to add default paddingOuter.
        return undefined;
    }
    if (isXorY(channel)) {
        const { bandPaddingOuter, bandWithNestedOffsetPaddingOuter } = scaleConfig;
        if (hasNestedOffsetScale) {
            return bandWithNestedOffsetPaddingOuter;
        }
        // Padding is only set for X and Y by default.
        // Basically it doesn't make sense to add padding for color and size.
        if (scaleType === ScaleType.BAND) {
            return getFirstDefined(bandPaddingOuter, 
            /* By default, paddingOuter is paddingInner / 2. The reason is that
              size (width/height) = step * (cardinality - paddingInner + 2 * paddingOuter).
              and we want the width/height to be integer by default.
              Note that step (by default) and cardinality are integers.) */
            isSignalRef(paddingInnerValue) ? { signal: `${paddingInnerValue.signal}/2` } : paddingInnerValue / 2);
        }
    }
    else if (isXorYOffset(channel)) {
        if (scaleType === ScaleType.POINT) {
            return 0.5; // so the point positions align with centers of band scales.
        }
        else if (scaleType === ScaleType.BAND) {
            return scaleConfig.offsetBandPaddingOuter;
        }
    }
    return undefined;
}
function reverse(scaleType, sort, channel, scaleConfig) {
    if (channel === 'x' && scaleConfig.xReverse !== undefined) {
        if (hasContinuousDomain(scaleType) && sort === 'descending') {
            if (isSignalRef(scaleConfig.xReverse)) {
                return { signal: `!${scaleConfig.xReverse.signal}` };
            }
            else {
                return !scaleConfig.xReverse;
            }
        }
        return scaleConfig.xReverse;
    }
    if (hasContinuousDomain(scaleType) && sort === 'descending') {
        // For continuous domain scales, Vega does not support domain sort.
        // Thus, we reverse range instead if sort is descending
        return true;
    }
    return undefined;
}
function zero(channel, fieldDef, specifiedDomain, markDef, scaleType, scaleConfig, hasSecondaryRangeChannel) {
    // If users explicitly provide a domain, we should not augment zero as that will be unexpected.
    const hasCustomDomain = !!specifiedDomain && specifiedDomain !== 'unaggregated';
    if (hasCustomDomain) {
        if (hasContinuousDomain(scaleType)) {
            if (isArray(specifiedDomain)) {
                const first = specifiedDomain[0];
                const last = specifiedDomain[specifiedDomain.length - 1];
                if (isNumber$1(first) && first <= 0 && isNumber$1(last) && last >= 0) {
                    // if the domain includes zero, make zero remain true
                    return true;
                }
            }
            return false;
        }
    }
    // If there is no custom domain, return configZero value (=`true` as default) only for the following cases:
    // 1) using quantitative field with size
    // While this can be either ratio or interval fields, our assumption is that
    // ratio are more common. However, if the scaleType is discretizing scale, we want to return
    // false so that range doesn't start at zero
    if (channel === 'size' && fieldDef.type === 'quantitative' && !isContinuousToDiscrete(scaleType)) {
        return true;
    }
    // 2) non-binned, quantitative x-scale or y-scale
    // (For binning, we should not include zero by default because binning are calculated without zero.)
    // (For area/bar charts with ratio scale chart, we should always include zero.)
    if (!(isFieldDef(fieldDef) && fieldDef.bin) &&
        contains([...POSITION_SCALE_CHANNELS, ...POLAR_POSITION_SCALE_CHANNELS], channel)) {
        const { orient, type } = markDef;
        if (contains(['bar', 'area', 'line', 'trail'], type)) {
            if ((orient === 'horizontal' && channel === 'y') || (orient === 'vertical' && channel === 'x')) {
                return false;
            }
        }
        if (contains(['bar', 'area'], type) && !hasSecondaryRangeChannel) {
            return true;
        }
        return scaleConfig?.zero;
    }
    return false;
}

/**
 * Determine if there is a specified scale type and if it is appropriate,
 * or determine default type if type is unspecified or inappropriate.
 */
// NOTE: CompassQL uses this method.
function scaleType(specifiedScale, channel, fieldDef, mark, hasNestedOffsetScale = false) {
    const defaultScaleType = defaultType(channel, fieldDef, mark, hasNestedOffsetScale);
    const { type } = specifiedScale;
    if (!isScaleChannel(channel)) {
        // There is no scale for these channels
        return null;
    }
    if (type !== undefined) {
        // Check if explicitly specified scale type is supported by the channel
        if (!channelSupportScaleType(channel, type)) {
            warn(scaleTypeNotWorkWithChannel(channel, type, defaultScaleType));
            return defaultScaleType;
        }
        // Check if explicitly specified scale type is supported by the data type
        if (isFieldDef(fieldDef) && !scaleTypeSupportDataType(type, fieldDef.type)) {
            warn(scaleTypeNotWorkWithFieldDef(type, defaultScaleType));
            return defaultScaleType;
        }
        return type;
    }
    return defaultScaleType;
}
/**
 * Determine appropriate default scale type.
 */
// NOTE: Voyager uses this method.
function defaultType(channel, fieldDef, mark, hasNestedOffsetScale) {
    switch (fieldDef.type) {
        case 'nominal':
        case 'ordinal': {
            if (isColorChannel(channel) || rangeType(channel) === 'discrete') {
                if (channel === 'shape' && fieldDef.type === 'ordinal') {
                    warn(discreteChannelCannotEncode(channel, 'ordinal'));
                }
                return 'ordinal';
            }
            if (isTime(channel)) {
                return 'band';
            }
            if (isXorY(channel) || isXorYOffset(channel)) {
                if (contains(['rect', 'bar', 'image', 'rule', 'tick'], mark.type)) {
                    // The rect/bar/tick mark should fit into a band.
                    // For rule, using band scale to make rule align with axis ticks better https://github.com/vega/vega-lite/issues/3429
                    return 'band';
                }
                if (hasNestedOffsetScale) {
                    // If there is a nested offset scale, then there is a "band" for the span of the nested scale.
                    return 'band';
                }
            }
            else if (mark.type === 'arc' && channel in POLAR_POSITION_SCALE_CHANNEL_INDEX) {
                return 'band';
            }
            const dimensionSize = mark[getSizeChannel(channel)];
            if (isRelativeBandSize(dimensionSize)) {
                return 'band';
            }
            if (isPositionFieldOrDatumDef(fieldDef) && fieldDef.axis?.tickBand) {
                return 'band';
            }
            // Otherwise, use ordinal point scale so we can easily get center positions of the marks.
            return 'point';
        }
        case 'temporal':
            if (isColorChannel(channel)) {
                return 'time';
            }
            else if (rangeType(channel) === 'discrete') {
                warn(discreteChannelCannotEncode(channel, 'temporal'));
                // TODO: consider using quantize (equivalent to binning) once we have it
                return 'ordinal';
            }
            else if (isFieldDef(fieldDef) && fieldDef.timeUnit && normalizeTimeUnit(fieldDef.timeUnit).utc) {
                return 'utc';
            }
            else if (isTime(channel)) {
                // return 'linear';
                return 'band'; // TODO(jzong): when interpolation is implemented, this should be 'linear'
            }
            return 'time';
        case 'quantitative':
            if (isColorChannel(channel)) {
                if (isFieldDef(fieldDef) && isBinning(fieldDef.bin)) {
                    return 'bin-ordinal';
                }
                return 'linear';
            }
            else if (rangeType(channel) === 'discrete') {
                warn(discreteChannelCannotEncode(channel, 'quantitative'));
                // TODO: consider using quantize (equivalent to binning) once we have it
                return 'ordinal';
            }
            else if (isTime(channel)) {
                // return 'linear';
                return 'band'; // TODO(jzong): when interpolation is implemented, this should be 'linear'
            }
            return 'linear';
        case 'geojson':
            return undefined;
    }
    /* istanbul ignore next: should never reach this */
    throw new Error(invalidFieldType(fieldDef.type));
}

function parseScales(model, { ignoreRange } = {}) {
    parseScaleCore(model);
    parseScaleDomain(model);
    for (const prop of NON_TYPE_DOMAIN_RANGE_VEGA_SCALE_PROPERTIES) {
        parseScaleProperty(model, prop);
    }
    if (!ignoreRange) {
        // range depends on zero
        parseScaleRange(model);
    }
}
function parseScaleCore(model) {
    if (isUnitModel(model)) {
        model.component.scales = parseUnitScaleCore(model);
    }
    else {
        model.component.scales = parseNonUnitScaleCore(model);
    }
}
/**
 * Parse scales for all channels of a model.
 */
function parseUnitScaleCore(model) {
    const { encoding, mark, markDef } = model;
    const scaleComponents = {};
    for (const channel of SCALE_CHANNELS) {
        const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]); // must be typed def to have scale
        // Don't generate scale for shape of geoshape
        if (fieldOrDatumDef && mark === GEOSHAPE && channel === SHAPE && fieldOrDatumDef.type === GEOJSON) {
            continue;
        }
        let specifiedScale = fieldOrDatumDef && fieldOrDatumDef.scale;
        if (fieldOrDatumDef && specifiedScale !== null && specifiedScale !== false) {
            specifiedScale ??= {};
            const hasNestedOffsetScale = channelHasNestedOffsetScale(encoding, channel);
            const sType = scaleType(specifiedScale, channel, fieldOrDatumDef, markDef, hasNestedOffsetScale);
            scaleComponents[channel] = new ScaleComponent(model.scaleName(`${channel}`, true), {
                value: sType,
                explicit: specifiedScale.type === sType,
            });
        }
    }
    return scaleComponents;
}
const scaleTypeTieBreaker = tieBreakByComparing((st1, st2) => scaleTypePrecedence(st1) - scaleTypePrecedence(st2));
function parseNonUnitScaleCore(model) {
    const scaleComponents = (model.component.scales = {});
    const scaleTypeWithExplicitIndex = {};
    const resolve = model.component.resolve;
    // Parse each child scale and determine if a particular channel can be merged.
    for (const child of model.children) {
        parseScaleCore(child);
        // Instead of always merging right away -- check if it is compatible to merge first!
        for (const channel of keys(child.component.scales)) {
            // if resolve is undefined, set default first
            resolve.scale[channel] ??= defaultScaleResolve(channel, model);
            if (resolve.scale[channel] === 'shared') {
                const explicitScaleType = scaleTypeWithExplicitIndex[channel];
                const childScaleType = child.component.scales[channel].getWithExplicit('type');
                if (explicitScaleType) {
                    if (scaleCompatible(explicitScaleType.value, childScaleType.value)) {
                        // merge scale component if type are compatible
                        scaleTypeWithExplicitIndex[channel] = mergeValuesWithExplicit(explicitScaleType, childScaleType, 'type', 'scale', scaleTypeTieBreaker);
                    }
                    else {
                        // Otherwise, update conflicting channel to be independent
                        resolve.scale[channel] = 'independent';
                        // Remove from the index so they don't get merged
                        delete scaleTypeWithExplicitIndex[channel];
                    }
                }
                else {
                    scaleTypeWithExplicitIndex[channel] = childScaleType;
                }
            }
        }
    }
    // Merge each channel listed in the index
    for (const channel of keys(scaleTypeWithExplicitIndex)) {
        // Create new merged scale component
        const name = model.scaleName(channel, true);
        const typeWithExplicit = scaleTypeWithExplicitIndex[channel];
        scaleComponents[channel] = new ScaleComponent(name, typeWithExplicit);
        // rename each child and mark them as merged
        for (const child of model.children) {
            const childScale = child.component.scales[channel];
            if (childScale) {
                child.renameScale(childScale.get('name'), name);
                childScale.merged = true;
            }
        }
    }
    return scaleComponents;
}

class NameMap {
    nameMap;
    constructor() {
        this.nameMap = {};
    }
    rename(oldName, newName) {
        this.nameMap[oldName] = newName;
    }
    has(name) {
        return this.nameMap[name] !== undefined;
    }
    get(name) {
        // If the name appears in the _nameMap, we need to read its new name.
        // We have to loop over the dict just in case the new name also gets renamed.
        while (this.nameMap[name] && name !== this.nameMap[name]) {
            name = this.nameMap[name];
        }
        return name;
    }
}
/*
  We use type guards instead of `instanceof` as `instanceof` makes
  different parts of the compiler depend on the actual implementation of
  the model classes, which in turn depend on different parts of the compiler.
  Thus, `instanceof` leads to circular dependency problems.

  On the other hand, type guards only make different parts of the compiler
  depend on the type of the model classes, but not the actual implementation.
*/
function isUnitModel(model) {
    return model?.type === 'unit';
}
function isFacetModel(model) {
    return model?.type === 'facet';
}
function isConcatModel(model) {
    return model?.type === 'concat';
}
function isLayerModel(model) {
    return model?.type === 'layer';
}
class Model {
    type;
    parent;
    config;
    name;
    size;
    title;
    description;
    data;
    transforms;
    layout;
    /** Name map for scales, which can be renamed by a model's parent. */
    scaleNameMap;
    /** Name map for projections, which can be renamed by a model's parent. */
    projectionNameMap;
    /** Name map for signals, which can be renamed by a model's parent. */
    signalNameMap;
    component;
    view;
    constructor(spec, type, parent, parentGivenName, config, resolve, view) {
        this.type = type;
        this.parent = parent;
        this.config = config;
        this.parent = parent;
        this.config = config;
        this.view = replaceExprRef(view);
        // If name is not provided, always use parent's givenName to avoid name conflicts.
        this.name = spec.name ?? parentGivenName;
        this.title = isText(spec.title) ? { text: spec.title } : spec.title ? replaceExprRef(spec.title) : undefined;
        // Shared name maps
        this.scaleNameMap = parent ? parent.scaleNameMap : new NameMap();
        this.projectionNameMap = parent ? parent.projectionNameMap : new NameMap();
        this.signalNameMap = parent ? parent.signalNameMap : new NameMap();
        this.data = spec.data;
        this.description = spec.description;
        this.transforms = normalizeTransform(spec.transform ?? []);
        this.layout = type === 'layer' || type === 'unit' ? {} : extractCompositionLayout(spec, type, config);
        this.component = {
            data: {
                sources: parent ? parent.component.data.sources : [],
                outputNodes: parent ? parent.component.data.outputNodes : {},
                outputNodeRefCounts: parent ? parent.component.data.outputNodeRefCounts : {},
                // data is faceted if the spec is a facet spec or the parent has faceted data and data is undefined
                isFaceted: isFacetSpec(spec) || (parent?.component.data.isFaceted && spec.data === undefined),
            },
            layoutSize: new Split(),
            layoutHeaders: { row: {}, column: {}, facet: {} },
            mark: null,
            resolve: {
                scale: {},
                axis: {},
                legend: {},
                ...(resolve ? duplicate(resolve) : {}),
            },
            selection: null,
            scales: null,
            projection: null,
            axes: {},
            legends: {},
        };
    }
    get width() {
        return this.getSizeSignalRef('width');
    }
    get height() {
        return this.getSizeSignalRef('height');
    }
    parse() {
        this.parseScale();
        this.parseLayoutSize(); // depends on scale
        this.renameTopLevelLayoutSizeSignal();
        this.parseSelections();
        this.parseProjection();
        this.parseData(); // (pathorder) depends on markDef; selection filters depend on parsed selections; depends on projection because some transforms require the finalized projection name.
        this.parseAxesAndHeaders(); // depends on scale and layout size
        this.parseLegends(); // depends on scale, markDef
        this.parseMarkGroup(); // depends on data name, scale, layout size, axisGroup, and children's scale, axis, legend and mark.
    }
    parseScale() {
        parseScales(this);
    }
    parseProjection() {
        parseProjection(this);
    }
    /**
     * Rename top-level spec's size to be just width / height, ignoring model name.
     * This essentially merges the top-level spec's width/height signals with the width/height signals
     * to help us reduce redundant signals declaration.
     */
    renameTopLevelLayoutSizeSignal() {
        if (this.getName('width') !== 'width') {
            this.renameSignal(this.getName('width'), 'width');
        }
        if (this.getName('height') !== 'height') {
            this.renameSignal(this.getName('height'), 'height');
        }
    }
    parseLegends() {
        parseLegend(this);
    }
    assembleEncodeFromView(view) {
        // Exclude "style"
        const { style: _, ...baseView } = view;
        const e = {};
        for (const property of keys(baseView)) {
            const value = baseView[property];
            if (value !== undefined) {
                e[property] = signalOrValueRef(value);
            }
        }
        return e;
    }
    assembleGroupEncodeEntry(isTopLevel) {
        let encodeEntry = {};
        if (this.view) {
            encodeEntry = this.assembleEncodeFromView(this.view);
        }
        if (!isTopLevel) {
            // Descriptions are already added to the top-level description so we only need to add them to the inner views.
            if (this.description) {
                encodeEntry['description'] = signalOrValueRef(this.description);
            }
            // For top-level spec, we can set the global width and height signal to adjust the group size.
            // For other child specs, we have to manually set width and height in the encode entry.
            if (this.type === 'unit' || this.type === 'layer') {
                return {
                    width: this.getSizeSignalRef('width'),
                    height: this.getSizeSignalRef('height'),
                    ...encodeEntry,
                };
            }
        }
        return isEmpty(encodeEntry) ? undefined : encodeEntry;
    }
    assembleLayout() {
        if (!this.layout) {
            return undefined;
        }
        const { spacing, ...layout } = this.layout;
        const { component, config } = this;
        const titleBand = assembleLayoutTitleBand(component.layoutHeaders, config);
        return {
            padding: spacing,
            ...this.assembleDefaultLayout(),
            ...layout,
            ...(titleBand ? { titleBand } : {}),
        };
    }
    assembleDefaultLayout() {
        return {};
    }
    assembleHeaderMarks() {
        const { layoutHeaders } = this.component;
        let headerMarks = [];
        for (const channel of FACET_CHANNELS) {
            if (layoutHeaders[channel].title) {
                headerMarks.push(assembleTitleGroup(this, channel));
            }
        }
        for (const channel of HEADER_CHANNELS) {
            headerMarks = headerMarks.concat(assembleHeaderGroups(this, channel));
        }
        return headerMarks;
    }
    assembleAxes() {
        return assembleAxes(this.component.axes, this.config);
    }
    assembleLegends() {
        return assembleLegends(this);
    }
    assembleProjections() {
        return assembleProjections(this);
    }
    assembleTitle() {
        const { encoding, ...titleNoEncoding } = this.title ?? {};
        const title = {
            ...extractTitleConfig(this.config.title).nonMarkTitleProperties,
            ...titleNoEncoding,
            ...(encoding ? { encode: { update: encoding } } : {}),
        };
        if (title.text) {
            if (contains(['unit', 'layer'], this.type)) {
                // Unit/Layer
                if (contains(['middle', undefined], title.anchor)) {
                    title.frame ??= 'group';
                }
            }
            else {
                // composition with Vega layout
                // Set title = "start" by default for composition as "middle" does not look nice
                // https://github.com/vega/vega/issues/960#issuecomment-471360328
                title.anchor ??= 'start';
            }
            return isEmpty(title) ? undefined : title;
        }
        return undefined;
    }
    /**
     * Assemble the mark group for this model. We accept optional `signals` so that we can include concat top-level signals with the top-level model's local signals.
     */
    assembleGroup(signals = []) {
        const group = {};
        signals = signals.concat(this.assembleSignals());
        if (signals.length > 0) {
            group.signals = signals;
        }
        const layout = this.assembleLayout();
        if (layout) {
            group.layout = layout;
        }
        group.marks = [].concat(this.assembleHeaderMarks(), this.assembleMarks());
        // Only include scales if this spec is top-level or if parent is facet.
        // (Otherwise, it will be merged with upper-level's scope.)
        const scales = !this.parent || isFacetModel(this.parent) ? assembleScales(this) : [];
        if (scales.length > 0) {
            group.scales = scales;
        }
        const axes = this.assembleAxes();
        if (axes.length > 0) {
            group.axes = axes;
        }
        const legends = this.assembleLegends();
        if (legends.length > 0) {
            group.legends = legends;
        }
        return group;
    }
    getName(text) {
        return varName((this.name ? `${this.name}_` : '') + text);
    }
    getDataName(type) {
        return this.getName(DataSourceType[type].toLowerCase());
    }
    /**
     * Request a data source name for the given data source type and mark that data source as required.
     * This method should be called in parse, so that all used data source can be correctly instantiated in assembleData().
     * You can lookup the correct dataset name in assemble with `lookupDataSource`.
     */
    requestDataName(name) {
        const fullName = this.getDataName(name);
        // Increase ref count. This is critical because otherwise we won't create a data source.
        // We also increase the ref counts on OutputNode.getSource() calls.
        const refCounts = this.component.data.outputNodeRefCounts;
        refCounts[fullName] = (refCounts[fullName] || 0) + 1;
        return fullName;
    }
    getSizeSignalRef(layoutSizeType) {
        if (isFacetModel(this.parent)) {
            const sizeType = getSizeTypeFromLayoutSizeType(layoutSizeType);
            const channel = getPositionScaleChannel(sizeType);
            const scaleComponent = this.component.scales[channel];
            if (scaleComponent && !scaleComponent.merged) {
                // independent scale
                const type = scaleComponent.get('type');
                const range = scaleComponent.get('range');
                if (hasDiscreteDomain(type) && isVgRangeStep(range)) {
                    const scaleName = scaleComponent.get('name');
                    const domain = assembleDomain(this, channel);
                    const field = getFieldFromDomain(domain);
                    if (field) {
                        const fieldRef = vgField({ aggregate: 'distinct', field }, { expr: 'datum' });
                        return {
                            signal: sizeExpr(scaleName, scaleComponent, fieldRef),
                        };
                    }
                    else {
                        warn(unknownField(channel));
                        return null;
                    }
                }
            }
        }
        return {
            signal: this.signalNameMap.get(this.getName(layoutSizeType)),
        };
    }
    /**
     * Lookup the name of the datasource for an output node. You probably want to call this in assemble.
     */
    lookupDataSource(name) {
        const node = this.component.data.outputNodes[name];
        if (!node) {
            // Name not found in map so let's just return what we got.
            // This can happen if we already have the correct name.
            return name;
        }
        return node.getSource();
    }
    getSignalName(oldSignalName) {
        return this.signalNameMap.get(oldSignalName);
    }
    renameSignal(oldName, newName) {
        this.signalNameMap.rename(oldName, newName);
    }
    renameScale(oldName, newName) {
        this.scaleNameMap.rename(oldName, newName);
    }
    renameProjection(oldName, newName) {
        this.projectionNameMap.rename(oldName, newName);
    }
    /**
     * @return scale name for a given channel after the scale has been parsed and named.
     */
    scaleName(originalScaleName, parse) {
        if (parse) {
            // During the parse phase always return a value
            // No need to refer to rename map because a scale can't be renamed
            // before it has the original name.
            return this.getName(originalScaleName);
        }
        // If there is a scale for the channel, it should either
        // be in the scale component or exist in the name map
        if (
        // If there is a scale for the channel, there should be a local scale component for it
        (isChannel(originalScaleName) && isScaleChannel(originalScaleName) && this.component.scales[originalScaleName]) ||
            // in the scale name map (the scale get merged by its parent)
            this.scaleNameMap.has(this.getName(originalScaleName))) {
            return this.scaleNameMap.get(this.getName(originalScaleName));
        }
        return undefined;
    }
    /**
     * @return projection name after the projection has been parsed and named.
     */
    projectionName(parse) {
        if (parse) {
            // During the parse phase always return a value
            // No need to refer to rename map because a projection can't be renamed
            // before it has the original name.
            return this.getName('projection');
        }
        if ((this.component.projection && !this.component.projection.merged) ||
            this.projectionNameMap.has(this.getName('projection'))) {
            return this.projectionNameMap.get(this.getName('projection'));
        }
        return undefined;
    }
    /**
     * Traverse a model's hierarchy to get the scale component for a particular channel.
     */
    getScaleComponent(channel) {
        /* istanbul ignore next: This is warning for debugging test */
        if (!this.component.scales) {
            throw new Error('getScaleComponent cannot be called before parseScale(). Make sure you have called parseScale or use parseUnitModelWithScale().');
        }
        const localScaleComponent = this.component.scales[channel];
        if (localScaleComponent && !localScaleComponent.merged) {
            return localScaleComponent;
        }
        return this.parent ? this.parent.getScaleComponent(channel) : undefined;
    }
    getScaleType(channel) {
        const scaleComponent = this.getScaleComponent(channel);
        return scaleComponent ? scaleComponent.get('type') : undefined;
    }
    /**
     * Traverse a model's hierarchy to get a particular selection component.
     */
    getSelectionComponent(variableName, origName) {
        let sel = this.component.selection[variableName];
        if (!sel && this.parent) {
            sel = this.parent.getSelectionComponent(variableName, origName);
        }
        if (!sel) {
            throw new Error(selectionNotFound(origName));
        }
        return sel;
    }
    /**
     * Returns true if the model has a signalRef for an axis orient.
     */
    hasAxisOrientSignalRef() {
        return (this.component.axes.x?.some((a) => a.hasOrientSignalRef()) ||
            this.component.axes.y?.some((a) => a.hasOrientSignalRef()));
    }
}
/** Abstract class for UnitModel and FacetModel. Both of which can contain fieldDefs as a part of its own specification. */
class ModelWithField extends Model {
    /** Get "field" reference for Vega */
    vgField(channel, opt = {}) {
        const fieldDef = this.fieldDef(channel);
        if (!fieldDef) {
            return undefined;
        }
        return vgField(fieldDef, opt);
    }
    reduceFieldDef(f, init) {
        return reduce(this.getMapping(), (acc, cd, c) => {
            const fieldDef = getFieldDef(cd);
            if (fieldDef) {
                return f(acc, fieldDef, c);
            }
            return acc;
        }, init);
    }
    forEachFieldDef(f, t) {
        forEach(this.getMapping(), (cd, c) => {
            const fieldDef = getFieldDef(cd);
            if (fieldDef) {
                f(fieldDef, c);
            }
        }, t);
    }
}

/**
 * A class for density transform nodes
 */
class DensityTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new DensityTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform); // duplicate to prevent side effects
        const specifiedAs = this.transform.as ?? [undefined, undefined];
        this.transform.as = [specifiedAs[0] ?? 'value', specifiedAs[1] ?? 'density'];
        const resolve = this.transform.resolve ?? 'shared';
        this.transform.resolve = resolve;
    }
    dependentFields() {
        return new Set([this.transform.density, ...(this.transform.groupby ?? [])]);
    }
    producedFields() {
        return new Set(this.transform.as);
    }
    hash() {
        return `DensityTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { density, ...rest } = this.transform;
        const result = {
            type: 'kde',
            field: density,
            ...rest,
        };
        result.resolve = this.transform.resolve;
        return result;
    }
}

/**
 * A class for flatten transform nodes
 */
class ExtentTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new ExtentTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform);
    }
    dependentFields() {
        return new Set([this.transform.extent]);
    }
    producedFields() {
        return new Set([]);
    }
    hash() {
        return `ExtentTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { extent, param } = this.transform;
        const result = {
            type: 'extent',
            field: extent,
            signal: param,
        };
        return result;
    }
}

/**
 * A class for flatten transform nodes
 */
class FlattenTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new FlattenTransformNode(this.parent, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform); // duplicate to prevent side effects
        const { flatten, as = [] } = this.transform;
        this.transform.as = flatten.map((f, i) => as[i] ?? f);
    }
    dependentFields() {
        return new Set(this.transform.flatten);
    }
    producedFields() {
        return new Set(this.transform.as);
    }
    hash() {
        return `FlattenTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { flatten: fields, as } = this.transform;
        const result = {
            type: 'flatten',
            fields,
            as,
        };
        return result;
    }
}

/**
 * A class for flatten transform nodes
 */
class FoldTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new FoldTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform); // duplicate to prevent side effects
        const specifiedAs = this.transform.as ?? [undefined, undefined];
        this.transform.as = [specifiedAs[0] ?? 'key', specifiedAs[1] ?? 'value'];
    }
    dependentFields() {
        return new Set(this.transform.fold);
    }
    producedFields() {
        return new Set(this.transform.as);
    }
    hash() {
        return `FoldTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { fold, as } = this.transform;
        const result = {
            type: 'fold',
            fields: fold,
            as,
        };
        return result;
    }
}

class GeoJSONNode extends DataFlowNode {
    fields;
    geojson;
    signal;
    clone() {
        return new GeoJSONNode(null, duplicate(this.fields), this.geojson, this.signal);
    }
    static parseAll(parent, model) {
        if (model.component.projection && !model.component.projection.isFit) {
            return parent;
        }
        let geoJsonCounter = 0;
        for (const coordinates of [
            [LONGITUDE, LATITUDE],
            [LONGITUDE2, LATITUDE2],
        ]) {
            const pair = coordinates.map((channel) => {
                const def = getFieldOrDatumDef(model.encoding[channel]);
                return isFieldDef(def)
                    ? def.field
                    : isDatumDef(def)
                        ? { expr: `${def.datum}` }
                        : isValueDef(def)
                            ? { expr: `${def['value']}` }
                            : undefined;
            });
            if (pair[0] || pair[1]) {
                parent = new GeoJSONNode(parent, pair, null, model.getName(`geojson_${geoJsonCounter++}`));
            }
        }
        if (model.channelHasField(SHAPE)) {
            const fieldDef = model.typedFieldDef(SHAPE);
            if (fieldDef.type === GEOJSON) {
                parent = new GeoJSONNode(parent, null, fieldDef.field, model.getName(`geojson_${geoJsonCounter++}`));
            }
        }
        return parent;
    }
    constructor(parent, fields, geojson, signal) {
        super(parent);
        this.fields = fields;
        this.geojson = geojson;
        this.signal = signal;
    }
    dependentFields() {
        const fields = (this.fields ?? []).filter(isString);
        return new Set([...(this.geojson ? [this.geojson] : []), ...fields]);
    }
    producedFields() {
        return new Set();
    }
    hash() {
        return `GeoJSON ${this.geojson} ${this.signal} ${hash(this.fields)}`;
    }
    assemble() {
        return [
            ...(this.geojson
                ? [
                    {
                        type: 'filter',
                        expr: `isValid(datum["${this.geojson}"])`,
                    },
                ]
                : []),
            {
                type: 'geojson',
                ...(this.fields ? { fields: this.fields } : {}),
                ...(this.geojson ? { geojson: this.geojson } : {}),
                signal: this.signal,
            },
        ];
    }
}

class GeoPointNode extends DataFlowNode {
    projection;
    fields;
    as;
    clone() {
        return new GeoPointNode(null, this.projection, duplicate(this.fields), duplicate(this.as));
    }
    constructor(parent, projection, fields, as) {
        super(parent);
        this.projection = projection;
        this.fields = fields;
        this.as = as;
    }
    static parseAll(parent, model) {
        if (!model.projectionName()) {
            return parent;
        }
        for (const coordinates of [
            [LONGITUDE, LATITUDE],
            [LONGITUDE2, LATITUDE2],
        ]) {
            const pair = coordinates.map((channel) => {
                const def = getFieldOrDatumDef(model.encoding[channel]);
                return isFieldDef(def)
                    ? def.field
                    : isDatumDef(def)
                        ? { expr: `${def.datum}` }
                        : isValueDef(def)
                            ? { expr: `${def['value']}` }
                            : undefined;
            });
            const suffix = coordinates[0] === LONGITUDE2 ? '2' : '';
            if (pair[0] || pair[1]) {
                parent = new GeoPointNode(parent, model.projectionName(), pair, [
                    model.getName(`x${suffix}`),
                    model.getName(`y${suffix}`),
                ]);
            }
        }
        return parent;
    }
    dependentFields() {
        return new Set(this.fields.filter(isString));
    }
    producedFields() {
        return new Set(this.as);
    }
    hash() {
        return `Geopoint ${this.projection} ${hash(this.fields)} ${hash(this.as)}`;
    }
    assemble() {
        return {
            type: 'geopoint',
            projection: this.projection,
            fields: this.fields,
            as: this.as,
        };
    }
}

class ImputeNode extends DataFlowNode {
    transform;
    clone() {
        return new ImputeNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
    }
    dependentFields() {
        return new Set([this.transform.impute, this.transform.key, ...(this.transform.groupby ?? [])]);
    }
    producedFields() {
        return new Set([this.transform.impute]);
    }
    processSequence(keyvals) {
        const { start = 0, stop, step } = keyvals;
        const result = [start, stop, ...(step ? [step] : [])].join(',');
        return { signal: `sequence(${result})` };
    }
    static makeFromTransform(parent, imputeTransform) {
        return new ImputeNode(parent, imputeTransform);
    }
    static makeFromEncoding(parent, model) {
        const encoding = model.encoding;
        const xDef = encoding.x;
        const yDef = encoding.y;
        if (isFieldDef(xDef) && isFieldDef(yDef)) {
            const imputedChannel = xDef.impute ? xDef : yDef.impute ? yDef : undefined;
            if (imputedChannel === undefined) {
                return undefined;
            }
            const keyChannel = xDef.impute ? yDef : yDef.impute ? xDef : undefined;
            const { method, value, frame, keyvals } = imputedChannel.impute;
            const groupbyFields = pathGroupingFields(model.mark, encoding);
            return new ImputeNode(parent, {
                impute: imputedChannel.field,
                key: keyChannel.field,
                ...(method ? { method } : {}),
                ...(value !== undefined ? { value } : {}),
                ...(frame ? { frame } : {}),
                ...(keyvals !== undefined ? { keyvals } : {}),
                ...(groupbyFields.length ? { groupby: groupbyFields } : {}),
            });
        }
        return null;
    }
    hash() {
        return `Impute ${hash(this.transform)}`;
    }
    assemble() {
        const { impute, key, keyvals, method, groupby, value, frame = [null, null] } = this.transform;
        const imputeTransform = {
            type: 'impute',
            field: impute,
            key,
            ...(keyvals ? { keyvals: isImputeSequence(keyvals) ? this.processSequence(keyvals) : keyvals } : {}),
            method: 'value',
            ...(groupby ? { groupby } : {}),
            value: !method || method === 'value' ? value : null,
        };
        if (method && method !== 'value') {
            const deriveNewField = {
                type: 'window',
                as: [`imputed_${impute}_value`],
                ops: [method],
                fields: [impute],
                frame,
                ignorePeers: false,
                ...(groupby ? { groupby } : {}),
            };
            const replaceOriginal = {
                type: 'formula',
                expr: `datum.${impute} === null ? datum.imputed_${impute}_value : datum.${impute}`,
                as: impute,
            };
            return [imputeTransform, deriveNewField, replaceOriginal];
        }
        else {
            return [imputeTransform];
        }
    }
}

/**
 * A class for loess transform nodes
 */
class LoessTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new LoessTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform); // duplicate to prevent side effects
        const specifiedAs = this.transform.as ?? [undefined, undefined];
        this.transform.as = [specifiedAs[0] ?? transform.on, specifiedAs[1] ?? transform.loess];
    }
    dependentFields() {
        return new Set([this.transform.loess, this.transform.on, ...(this.transform.groupby ?? [])]);
    }
    producedFields() {
        return new Set(this.transform.as);
    }
    hash() {
        return `LoessTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { loess, on, ...rest } = this.transform;
        const result = {
            type: 'loess',
            x: on,
            y: loess,
            ...rest,
        };
        return result;
    }
}

class LookupNode extends DataFlowNode {
    transform;
    secondary;
    clone() {
        return new LookupNode(null, duplicate(this.transform), this.secondary);
    }
    constructor(parent, transform, secondary) {
        super(parent);
        this.transform = transform;
        this.secondary = secondary;
    }
    static make(parent, model, transform, counter) {
        const sources = model.component.data.sources;
        const { from } = transform;
        let fromOutputNode = null;
        if (isLookupData(from)) {
            let fromSource = findSource(from.data, sources);
            if (!fromSource) {
                fromSource = new SourceNode(from.data);
                sources.push(fromSource);
            }
            const fromOutputName = model.getName(`lookup_${counter}`);
            fromOutputNode = new OutputNode(fromSource, fromOutputName, DataSourceType.Lookup, model.component.data.outputNodeRefCounts);
            model.component.data.outputNodes[fromOutputName] = fromOutputNode;
        }
        else if (isLookupSelection(from)) {
            const selName = from.param;
            transform = { as: selName, ...transform };
            let selCmpt;
            try {
                selCmpt = model.getSelectionComponent(varName(selName), selName);
            }
            catch {
                throw new Error(cannotLookupVariableParameter(selName));
            }
            fromOutputNode = selCmpt.materialized;
            if (!fromOutputNode) {
                throw new Error(noSameUnitLookup(selName));
            }
        }
        return new LookupNode(parent, transform, fromOutputNode.getSource());
    }
    dependentFields() {
        return new Set([this.transform.lookup]);
    }
    producedFields() {
        return new Set(this.transform.as ? array$2(this.transform.as) : this.transform.from.fields);
    }
    hash() {
        return `Lookup ${hash({ transform: this.transform, secondary: this.secondary })}`;
    }
    assemble() {
        let foreign;
        if (this.transform.from.fields) {
            // lookup a few fields and add create a flat output
            foreign = {
                values: this.transform.from.fields,
                ...(this.transform.as ? { as: array$2(this.transform.as) } : {}),
            };
        }
        else {
            // lookup full record and nest it
            let asName = this.transform.as;
            if (!isString(asName)) {
                warn(NO_FIELDS_NEEDS_AS);
                asName = '_lookup';
            }
            foreign = {
                as: [asName],
            };
        }
        return {
            type: 'lookup',
            from: this.secondary,
            key: this.transform.from.key,
            fields: [this.transform.lookup],
            ...foreign,
            ...(this.transform.default ? { default: this.transform.default } : {}),
        };
    }
}

/**
 * A class for quantile transform nodes
 */
class QuantileTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new QuantileTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform); // duplicate to prevent side effects
        const specifiedAs = this.transform.as ?? [undefined, undefined];
        this.transform.as = [specifiedAs[0] ?? 'prob', specifiedAs[1] ?? 'value'];
    }
    dependentFields() {
        return new Set([this.transform.quantile, ...(this.transform.groupby ?? [])]);
    }
    producedFields() {
        return new Set(this.transform.as);
    }
    hash() {
        return `QuantileTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { quantile, ...rest } = this.transform;
        const result = {
            type: 'quantile',
            field: quantile,
            ...rest,
        };
        return result;
    }
}

/**
 * A class for regression transform nodes
 */
class RegressionTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new RegressionTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
        this.transform = duplicate(transform); // duplicate to prevent side effects
        const specifiedAs = this.transform.as ?? [undefined, undefined];
        this.transform.as = [specifiedAs[0] ?? transform.on, specifiedAs[1] ?? transform.regression];
    }
    dependentFields() {
        return new Set([this.transform.regression, this.transform.on, ...(this.transform.groupby ?? [])]);
    }
    producedFields() {
        return new Set(this.transform.as);
    }
    hash() {
        return `RegressionTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { regression, on, ...rest } = this.transform;
        const result = {
            type: 'regression',
            x: on,
            y: regression,
            ...rest,
        };
        return result;
    }
}

/**
 * A class for pivot transform nodes.
 */
class PivotTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new PivotTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
    }
    addDimensions(fields) {
        this.transform.groupby = unique((this.transform.groupby ?? []).concat(fields), (d) => d);
    }
    producedFields() {
        return undefined; // return undefined so that potentially everything can depend on the pivot
    }
    dependentFields() {
        return new Set([this.transform.pivot, this.transform.value, ...(this.transform.groupby ?? [])]);
    }
    hash() {
        return `PivotTransform ${hash(this.transform)}`;
    }
    assemble() {
        const { pivot, value, groupby, limit, op } = this.transform;
        return {
            type: 'pivot',
            field: pivot,
            value,
            ...(limit !== undefined ? { limit } : {}),
            ...(op !== undefined ? { op } : {}),
            ...(groupby !== undefined ? { groupby } : {}),
        };
    }
}

/**
 * A class for the sample transform nodes
 */
class SampleTransformNode extends DataFlowNode {
    transform;
    clone() {
        return new SampleTransformNode(null, duplicate(this.transform));
    }
    constructor(parent, transform) {
        super(parent);
        this.transform = transform;
    }
    dependentFields() {
        return new Set();
    }
    producedFields() {
        return new Set();
    }
    hash() {
        return `SampleTransform ${hash(this.transform)}`;
    }
    assemble() {
        return {
            type: 'sample',
            size: this.transform.sample,
        };
    }
}

function makeWalkTree(data) {
    // to name datasources
    let datasetIndex = 0;
    /**
     * Recursively walk down the tree.
     */
    function walkTree(node, dataSource) {
        if (node instanceof SourceNode) {
            // If the source is a named data source or a data source with values, we need
            // to put it in a different data source. Otherwise, Vega may override the data.
            if (!node.isGenerator && !isUrlData(node.data)) {
                data.push(dataSource);
                const newData = {
                    name: null,
                    source: dataSource.name,
                    transform: [],
                };
                dataSource = newData;
            }
        }
        if (node instanceof ParseNode) {
            if (node.parent instanceof SourceNode && !dataSource.source) {
                // If node's parent is a root source and the data source does not refer to another data source, use normal format parse
                dataSource.format = {
                    ...dataSource.format,
                    parse: node.assembleFormatParse(),
                };
                // add calculates for all nested fields
                dataSource.transform.push(...node.assembleTransforms(true));
            }
            else {
                // Otherwise use Vega expression to parse
                dataSource.transform.push(...node.assembleTransforms());
            }
        }
        if (node instanceof FacetNode) {
            if (!dataSource.name) {
                dataSource.name = `data_${datasetIndex++}`;
            }
            if (!dataSource.source || dataSource.transform.length > 0) {
                data.push(dataSource);
                node.data = dataSource.name;
            }
            else {
                node.data = dataSource.source;
            }
            data.push(...node.assemble());
            // break here because the rest of the tree has to be taken care of by the facet.
            return;
        }
        if (node instanceof GraticuleNode ||
            node instanceof SequenceNode ||
            node instanceof FilterInvalidNode ||
            node instanceof FilterNode ||
            node instanceof CalculateNode ||
            node instanceof GeoPointNode ||
            node instanceof AggregateNode ||
            node instanceof LookupNode ||
            node instanceof WindowTransformNode ||
            node instanceof JoinAggregateTransformNode ||
            node instanceof FoldTransformNode ||
            node instanceof FlattenTransformNode ||
            node instanceof DensityTransformNode ||
            node instanceof LoessTransformNode ||
            node instanceof QuantileTransformNode ||
            node instanceof RegressionTransformNode ||
            node instanceof IdentifierNode ||
            node instanceof SampleTransformNode ||
            node instanceof PivotTransformNode ||
            node instanceof ExtentTransformNode) {
            dataSource.transform.push(node.assemble());
        }
        if (node instanceof BinNode ||
            node instanceof TimeUnitNode ||
            node instanceof ImputeNode ||
            node instanceof StackNode ||
            node instanceof GeoJSONNode) {
            dataSource.transform.push(...node.assemble());
        }
        if (node instanceof OutputNode) {
            if (dataSource.source && dataSource.transform.length === 0) {
                node.setSource(dataSource.source);
            }
            else if (node.parent instanceof OutputNode) {
                // Note that an output node may be required but we still do not assemble a
                // separate data source for it.
                node.setSource(dataSource.name);
            }
            else {
                if (!dataSource.name) {
                    dataSource.name = `data_${datasetIndex++}`;
                }
                // Here we set the name of the datasource we generated. From now on
                // other assemblers can use it.
                node.setSource(dataSource.name);
                // if this node has more than one child, we will add a datasource automatically
                if (node.numChildren() === 1) {
                    data.push(dataSource);
                    const newData = {
                        name: null,
                        source: dataSource.name,
                        transform: [],
                    };
                    dataSource = newData;
                }
            }
        }
        switch (node.numChildren()) {
            case 0:
                // done
                if (node instanceof OutputNode && (!dataSource.source || dataSource.transform.length > 0)) {
                    // do not push empty datasources that are simply references
                    data.push(dataSource);
                }
                break;
            case 1:
                walkTree(node.children[0], dataSource);
                break;
            default: {
                if (!dataSource.name) {
                    dataSource.name = `data_${datasetIndex++}`;
                }
                let source = dataSource.name;
                if (!dataSource.source || dataSource.transform.length > 0) {
                    data.push(dataSource);
                }
                else {
                    source = dataSource.source;
                }
                for (const child of node.children) {
                    const newData = {
                        name: null,
                        source,
                        transform: [],
                    };
                    walkTree(child, newData);
                }
                break;
            }
        }
    }
    return walkTree;
}
/**
 * Assemble data sources that are derived from faceted data.
 */
function assembleFacetData(root) {
    const data = [];
    const walkTree = makeWalkTree(data);
    for (const child of root.children) {
        walkTree(child, {
            source: root.name,
            name: null,
            transform: [],
        });
    }
    return data;
}
/**
 * Create Vega data array from a given compiled model and append all of them to the given array
 *
 * @param  model
 * @param  data array
 * @return modified data array
 */
function assembleRootData(dataComponent, datasets) {
    const data = [];
    // dataComponent.sources.forEach(debug);
    // draw(dataComponent.sources);
    const walkTree = makeWalkTree(data);
    let sourceIndex = 0;
    for (const root of dataComponent.sources) {
        // assign a name if the source does not have a name yet
        if (!root.hasName()) {
            root.dataName = `source_${sourceIndex++}`;
        }
        const newData = root.assemble();
        walkTree(root, newData);
    }
    // remove empty transform arrays for cleaner output
    for (const d of data) {
        if (d.transform.length === 0) {
            delete d.transform;
        }
    }
    // move sources without transforms (the ones that are potentially used in lookups) to the beginning
    let whereTo = 0;
    for (const [i, d] of data.entries()) {
        if ((d.transform ?? []).length === 0 && !d.source) {
            data.splice(whereTo++, 0, data.splice(i, 1)[0]);
        }
    }
    // now fix the from references in lookup transforms
    for (const d of data) {
        for (const t of d.transform ?? []) {
            if (t.type === 'lookup') {
                t.from = dataComponent.outputNodes[t.from].getSource();
            }
        }
    }
    // inline values for datasets that are in the datastore
    for (const d of data) {
        if (d.name in datasets) {
            d.values = datasets[d.name];
        }
    }
    return data;
}

function getHeaderType(orient) {
    if (orient === 'top' || orient === 'left' || isSignalRef(orient)) {
        // we always use header for orient signal since we can't dynamically make header becomes footer
        return 'header';
    }
    return 'footer';
}
function parseFacetHeaders(model) {
    for (const channel of FACET_CHANNELS) {
        parseFacetHeader(model, channel);
    }
    mergeChildAxis(model, 'x');
    mergeChildAxis(model, 'y');
}
function parseFacetHeader(model, channel) {
    const { facet, config, child, component } = model;
    if (model.channelHasField(channel)) {
        const fieldDef = facet[channel];
        const titleConfig = getHeaderProperty('title', null, config, channel);
        let title$1 = title(fieldDef, config, {
            allowDisabling: true,
            includeDefault: titleConfig === undefined || !!titleConfig,
        });
        if (child.component.layoutHeaders[channel].title) {
            // TODO: better handle multiline titles
            title$1 = isArray(title$1) ? title$1.join(', ') : title$1;
            // merge title with child to produce "Title / Subtitle / Sub-subtitle"
            title$1 += ` / ${child.component.layoutHeaders[channel].title}`;
            child.component.layoutHeaders[channel].title = null;
        }
        const labelOrient = getHeaderProperty('labelOrient', fieldDef.header, config, channel);
        const labels = fieldDef.header !== null ? getFirstDefined(fieldDef.header?.labels, config.header.labels, true) : false;
        const headerType = contains(['bottom', 'right'], labelOrient) ? 'footer' : 'header';
        component.layoutHeaders[channel] = {
            title: fieldDef.header !== null ? title$1 : null,
            facetFieldDef: fieldDef,
            [headerType]: channel === 'facet' ? [] : [makeHeaderComponent(model, channel, labels)],
        };
    }
}
function makeHeaderComponent(model, channel, labels) {
    const sizeType = channel === 'row' ? 'height' : 'width';
    return {
        labels,
        sizeSignal: model.child.component.layoutSize.get(sizeType) ? model.child.getSizeSignalRef(sizeType) : undefined,
        axes: [],
    };
}
function mergeChildAxis(model, channel) {
    const { child } = model;
    if (child.component.axes[channel]) {
        const { layoutHeaders, resolve } = model.component;
        resolve.axis[channel] = parseGuideResolve(resolve, channel);
        if (resolve.axis[channel] === 'shared') {
            // For shared axis, move the axes to facet's header or footer
            const headerChannel = channel === 'x' ? 'column' : 'row';
            const layoutHeader = layoutHeaders[headerChannel];
            for (const axisComponent of child.component.axes[channel]) {
                const headerType = getHeaderType(axisComponent.get('orient'));
                layoutHeader[headerType] ??= [makeHeaderComponent(model, headerChannel, false)];
                // FIXME: assemble shouldn't be called here, but we do it this way so we only extract the main part of the axes
                const mainAxis = assembleAxis(axisComponent, 'main', model.config, { header: true });
                if (mainAxis) {
                    // LayoutHeader no longer keep track of property precedence, thus let's combine.
                    layoutHeader[headerType][0].axes.push(mainAxis);
                }
                axisComponent.mainExtracted = true;
            }
        }
    }
}

function parseLayerLayoutSize(model) {
    parseChildrenLayoutSize(model);
    parseNonUnitLayoutSizeForChannel(model, 'width');
    parseNonUnitLayoutSizeForChannel(model, 'height');
}
function parseConcatLayoutSize(model) {
    parseChildrenLayoutSize(model);
    // for columns === 1 (vconcat), we can completely merge width. Otherwise, we can treat merged width as childWidth.
    const widthType = model.layout.columns === 1 ? 'width' : 'childWidth';
    // for columns === undefined (hconcat), we can completely merge height. Otherwise, we can treat merged height as childHeight.
    const heightType = model.layout.columns === undefined ? 'height' : 'childHeight';
    parseNonUnitLayoutSizeForChannel(model, widthType);
    parseNonUnitLayoutSizeForChannel(model, heightType);
}
function parseChildrenLayoutSize(model) {
    for (const child of model.children) {
        child.parseLayoutSize();
    }
}
/**
 * Merge child layout size (width or height).
 */
function parseNonUnitLayoutSizeForChannel(model, layoutSizeType) {
    /*
     * For concat, the parent width or height might not be the same as the children's shared height.
     * For example, hconcat's subviews may share width, but the shared width is not the hconcat view's width.
     *
     * layoutSizeType represents the output of the view (could be childWidth/childHeight/width/height)
     * while the sizeType represents the properties of the child.
     */
    const sizeType = getSizeTypeFromLayoutSizeType(layoutSizeType);
    const channel = getPositionScaleChannel(sizeType);
    const resolve = model.component.resolve;
    const layoutSizeCmpt = model.component.layoutSize;
    let mergedSize;
    // Try to merge layout size
    for (const child of model.children) {
        const childSize = child.component.layoutSize.getWithExplicit(sizeType);
        const scaleResolve = resolve.scale[channel] ?? defaultScaleResolve(channel, model);
        if (scaleResolve === 'independent' && childSize.value === 'step') {
            // Do not merge independent scales with range-step as their size depends
            // on the scale domains, which can be different between scales.
            mergedSize = undefined;
            break;
        }
        if (mergedSize) {
            if (scaleResolve === 'independent' && mergedSize.value !== childSize.value) {
                // For independent scale, only merge if all the sizes are the same.
                // If the values are different, abandon the merge!
                mergedSize = undefined;
                break;
            }
            mergedSize = mergeValuesWithExplicit(mergedSize, childSize, sizeType, '');
        }
        else {
            mergedSize = childSize;
        }
    }
    if (mergedSize) {
        // If merged, rename size and set size of all children.
        for (const child of model.children) {
            model.renameSignal(child.getName(sizeType), model.getName(layoutSizeType));
            child.component.layoutSize.set(sizeType, 'merged', false);
        }
        layoutSizeCmpt.setWithExplicit(layoutSizeType, mergedSize);
    }
    else {
        layoutSizeCmpt.setWithExplicit(layoutSizeType, {
            explicit: false,
            value: undefined,
        });
    }
}
function parseUnitLayoutSize(model) {
    const { size, component } = model;
    for (const channel of POSITION_SCALE_CHANNELS) {
        const sizeType = getSizeChannel(channel);
        if (size[sizeType] != undefined && size[sizeType] != null) {
            const specifiedSize = size[sizeType];
            component.layoutSize.set(sizeType, isStep(specifiedSize) ? 'step' : specifiedSize, true);
        }
        else {
            const defaultSize = defaultUnitSize(model, sizeType);
            component.layoutSize.set(sizeType, defaultSize, false);
        }
    }
}
function defaultUnitSize(model, sizeType) {
    const channel = sizeType === 'width' ? 'x' : 'y';
    const config = model.config;
    const scaleComponent = model.getScaleComponent(channel);
    if (scaleComponent) {
        const scaleType = scaleComponent.get('type');
        const range = scaleComponent.get('range');
        if (hasDiscreteDomain(scaleType)) {
            const size = getViewConfigDiscreteSize(config.view, sizeType);
            if (isVgRangeStep(range) || isStep(size)) {
                // For discrete domain with range.step, use dynamic width/height
                return 'step';
            }
            else {
                return size;
            }
        }
        else {
            return getViewConfigContinuousSize(config.view, sizeType);
        }
    }
    else if (model.hasProjection || model.mark === 'arc') {
        // arc should use continuous size by default otherwise the pie is extremely small
        return getViewConfigContinuousSize(config.view, sizeType);
    }
    else {
        const size = getViewConfigDiscreteSize(config.view, sizeType);
        return isStep(size) ? size.step : size;
    }
}

function facetSortFieldName(fieldDef, sort, opt) {
    return vgField(sort, { suffix: `by_${vgField(fieldDef)}`, ...opt });
}
class FacetModel extends ModelWithField {
    facet;
    child;
    children;
    constructor(spec, parent, parentGivenName, config) {
        super(spec, 'facet', parent, parentGivenName, config, spec.resolve);
        this.child = buildModel(spec.spec, this, this.getName('child'), undefined, config);
        this.children = [this.child];
        this.facet = this.initFacet(spec.facet);
    }
    initFacet(facet) {
        // clone to prevent side effect to the original spec
        if (!isFacetMapping(facet)) {
            return { facet: this.initFacetFieldDef(facet, 'facet') };
        }
        const channels = keys(facet);
        const normalizedFacet = {};
        for (const channel of channels) {
            if (![ROW, COLUMN].includes(channel)) {
                // Drop unsupported channel
                warn(incompatibleChannel(channel, 'facet'));
                break;
            }
            const fieldDef = facet[channel];
            if (fieldDef.field === undefined) {
                warn(emptyFieldDef(fieldDef, channel));
                break;
            }
            normalizedFacet[channel] = this.initFacetFieldDef(fieldDef, channel);
        }
        return normalizedFacet;
    }
    initFacetFieldDef(fieldDef, channel) {
        // Cast because we call initFieldDef, which assumes general FieldDef.
        // However, FacetFieldDef is a bit more constrained than the general FieldDef
        const facetFieldDef = initFieldDef(fieldDef, channel);
        if (facetFieldDef.header) {
            facetFieldDef.header = replaceExprRef(facetFieldDef.header);
        }
        else if (facetFieldDef.header === null) {
            facetFieldDef.header = null;
        }
        return facetFieldDef;
    }
    channelHasField(channel) {
        return hasProperty(this.facet, channel);
    }
    fieldDef(channel) {
        return this.facet[channel];
    }
    parseData() {
        this.component.data = parseData(this);
        this.child.parseData();
    }
    parseLayoutSize() {
        parseChildrenLayoutSize(this);
    }
    parseSelections() {
        // As a facet has a single child, the selection components are the same.
        // The child maintains its selections to assemble signals, which remain
        // within its unit.
        this.child.parseSelections();
        this.component.selection = this.child.component.selection;
        if (vals(this.component.selection).some((selCmpt) => isTimerSelection(selCmpt))) {
            error(MULTI_VIEW_ANIMATION_UNSUPPORTED);
        }
    }
    parseMarkGroup() {
        this.child.parseMarkGroup();
    }
    parseAxesAndHeaders() {
        this.child.parseAxesAndHeaders();
        parseFacetHeaders(this);
    }
    assembleSelectionTopLevelSignals(signals) {
        return this.child.assembleSelectionTopLevelSignals(signals);
    }
    assembleSignals() {
        this.child.assembleSignals();
        return [];
    }
    assembleSelectionData(data) {
        return this.child.assembleSelectionData(data);
    }
    getHeaderLayoutMixins() {
        const layoutMixins = {};
        for (const channel of FACET_CHANNELS) {
            for (const headerType of HEADER_TYPES) {
                const layoutHeaderComponent = this.component.layoutHeaders[channel];
                const headerComponent = layoutHeaderComponent[headerType];
                const { facetFieldDef } = layoutHeaderComponent;
                if (facetFieldDef) {
                    const titleOrient = getHeaderProperty('titleOrient', facetFieldDef.header, this.config, channel);
                    if (['right', 'bottom'].includes(titleOrient)) {
                        const headerChannel = getHeaderChannel(channel, titleOrient);
                        layoutMixins.titleAnchor ??= {};
                        layoutMixins.titleAnchor[headerChannel] = 'end';
                    }
                }
                if (headerComponent?.[0]) {
                    // set header/footerBand
                    const sizeType = channel === 'row' ? 'height' : 'width';
                    const bandType = headerType === 'header' ? 'headerBand' : 'footerBand';
                    if (channel !== 'facet' && !this.child.component.layoutSize.get(sizeType)) {
                        // If facet child does not have size signal, then apply headerBand
                        layoutMixins[bandType] ??= {};
                        layoutMixins[bandType][channel] = 0.5;
                    }
                    if (layoutHeaderComponent.title) {
                        layoutMixins.offset ??= {};
                        layoutMixins.offset[channel === 'row' ? 'rowTitle' : 'columnTitle'] = 10;
                    }
                }
            }
        }
        return layoutMixins;
    }
    assembleDefaultLayout() {
        const { column, row } = this.facet;
        const columns = column ? this.columnDistinctSignal() : row ? 1 : undefined;
        let align = 'all';
        // Do not align the cells if the scale corresponding to the direction is indepent.
        // We always align when we facet into both row and column.
        if (!row && this.component.resolve.scale.x === 'independent') {
            align = 'none';
        }
        else if (!column && this.component.resolve.scale.y === 'independent') {
            align = 'none';
        }
        return {
            ...this.getHeaderLayoutMixins(),
            ...(columns ? { columns } : {}),
            bounds: 'full',
            align,
        };
    }
    assembleLayoutSignals() {
        // FIXME(https://github.com/vega/vega-lite/issues/1193): this can be incorrect if we have independent scales.
        return this.child.assembleLayoutSignals();
    }
    columnDistinctSignal() {
        if (this.parent && this.parent instanceof FacetModel) {
            // For nested facet, we will add columns to group mark instead
            // See discussion in https://github.com/vega/vega/issues/952
            // and https://github.com/vega/vega-view/releases/tag/v1.2.6
            return undefined;
        }
        else {
            // In facetNode.assemble(), the name is always this.getName('column') + '_layout'.
            const facetLayoutDataName = this.getName('column_domain');
            return { signal: `length(data('${facetLayoutDataName}'))` };
        }
    }
    assembleGroupStyle() {
        return undefined;
    }
    assembleGroup(signals) {
        if (this.parent && this.parent instanceof FacetModel) {
            // Provide number of columns for layout.
            // See discussion in https://github.com/vega/vega/issues/952
            // and https://github.com/vega/vega-view/releases/tag/v1.2.6
            return {
                ...(this.channelHasField('column')
                    ? {
                        encode: {
                            update: {
                                // TODO(https://github.com/vega/vega-lite/issues/2759):
                                // Correct the signal for facet of concat of facet_column
                                columns: { field: vgField(this.facet.column, { prefix: 'distinct' }) },
                            },
                        },
                    }
                    : {}),
                ...super.assembleGroup(signals),
            };
        }
        return super.assembleGroup(signals);
    }
    /**
     * Aggregate cardinality for calculating size
     */
    getCardinalityAggregateForChild() {
        const fields = [];
        const ops = [];
        const as = [];
        if (this.child instanceof FacetModel) {
            if (this.child.channelHasField('column')) {
                const field = vgField(this.child.facet.column);
                fields.push(field);
                ops.push('distinct');
                as.push(`distinct_${field}`);
            }
        }
        else {
            for (const channel of POSITION_SCALE_CHANNELS) {
                const childScaleComponent = this.child.component.scales[channel];
                if (childScaleComponent && !childScaleComponent.merged) {
                    const type = childScaleComponent.get('type');
                    const range = childScaleComponent.get('range');
                    if (hasDiscreteDomain(type) && isVgRangeStep(range)) {
                        const domain = assembleDomain(this.child, channel);
                        const field = getFieldFromDomain(domain);
                        if (field) {
                            fields.push(field);
                            ops.push('distinct');
                            as.push(`distinct_${field}`);
                        }
                        else {
                            warn(unknownField(channel));
                        }
                    }
                }
            }
        }
        return { fields, ops, as };
    }
    assembleFacet() {
        const { name, data } = this.component.data.facetRoot;
        const { row, column } = this.facet;
        const { fields, ops, as } = this.getCardinalityAggregateForChild();
        const groupby = [];
        for (const channel of FACET_CHANNELS) {
            const fieldDef = this.facet[channel];
            if (fieldDef) {
                groupby.push(vgField(fieldDef));
                const { bin, sort } = fieldDef;
                if (isBinning(bin)) {
                    groupby.push(vgField(fieldDef, { binSuffix: 'end' }));
                }
                if (isSortField(sort)) {
                    const { field, op = DEFAULT_SORT_OP } = sort;
                    const outputName = facetSortFieldName(fieldDef, sort);
                    if (row && column) {
                        // For crossed facet, use pre-calculate field as it requires a different groupby
                        // For each calculated field, apply max and assign them to the same name as
                        // all values of the same group should be the same anyway.
                        fields.push(outputName);
                        ops.push('max');
                        as.push(outputName);
                    }
                    else {
                        fields.push(field);
                        ops.push(op);
                        as.push(outputName);
                    }
                }
                else if (isArray(sort)) {
                    const outputName = sortArrayIndexField(fieldDef, channel);
                    fields.push(outputName);
                    ops.push('max');
                    as.push(outputName);
                }
            }
        }
        const cross = !!row && !!column;
        return {
            name,
            data,
            groupby,
            ...(cross || fields.length > 0
                ? {
                    aggregate: {
                        ...(cross ? { cross } : {}),
                        ...(fields.length ? { fields, ops, as } : {}),
                    },
                }
                : {}),
        };
    }
    facetSortFields(channel) {
        const { facet } = this;
        const fieldDef = facet[channel];
        if (fieldDef) {
            if (isSortField(fieldDef.sort)) {
                return [facetSortFieldName(fieldDef, fieldDef.sort, { expr: 'datum' })];
            }
            else if (isArray(fieldDef.sort)) {
                return [sortArrayIndexField(fieldDef, channel, { expr: 'datum' })];
            }
            return [vgField(fieldDef, { expr: 'datum' })];
        }
        return [];
    }
    facetSortOrder(channel) {
        const { facet } = this;
        const fieldDef = facet[channel];
        if (fieldDef) {
            const { sort } = fieldDef;
            const order = (isSortField(sort) ? sort.order : !isArray(sort) && sort) || 'ascending';
            return [order];
        }
        return [];
    }
    assembleLabelTitle() {
        const { facet, config } = this;
        if (facet.facet) {
            // Facet always uses title to display labels
            return assembleLabelTitle(facet.facet, 'facet', config);
        }
        const ORTHOGONAL_ORIENT = {
            row: ['top', 'bottom'],
            column: ['left', 'right'],
        };
        for (const channel of HEADER_CHANNELS) {
            if (facet[channel]) {
                const labelOrient = getHeaderProperty('labelOrient', facet[channel]?.header, config, channel);
                if (ORTHOGONAL_ORIENT[channel].includes(labelOrient)) {
                    // Row/Column with orthogonal labelOrient must use title to display labels
                    return assembleLabelTitle(facet[channel], channel, config);
                }
            }
        }
        return undefined;
    }
    assembleMarks() {
        const { child } = this;
        // If we facet by two dimensions, we need to add a cross operator to the aggregation
        // so that we create all groups
        const facetRoot = this.component.data.facetRoot;
        const data = assembleFacetData(facetRoot);
        const encodeEntry = child.assembleGroupEncodeEntry(false);
        const title = this.assembleLabelTitle() || child.assembleTitle();
        const style = child.assembleGroupStyle();
        const markGroup = {
            name: this.getName('cell'),
            type: 'group',
            ...(title ? { title } : {}),
            ...(style ? { style } : {}),
            from: {
                facet: this.assembleFacet(),
            },
            // TODO: move this to after data
            sort: {
                field: FACET_CHANNELS.map((c) => this.facetSortFields(c)).flat(),
                order: FACET_CHANNELS.map((c) => this.facetSortOrder(c)).flat(),
            },
            ...(data.length > 0 ? { data } : {}),
            ...(encodeEntry ? { encode: { update: encodeEntry } } : {}),
            ...child.assembleGroup(assembleFacetSignals(this, [])),
        };
        return [markGroup];
    }
    getMapping() {
        return this.facet;
    }
}

function makeJoinAggregateFromFacet(parent, facet) {
    const { row, column } = facet;
    if (row && column) {
        let newParent = null;
        // only need to make one for crossed facet
        for (const fieldDef of [row, column]) {
            if (isSortField(fieldDef.sort)) {
                const { field, op = DEFAULT_SORT_OP } = fieldDef.sort;
                parent = newParent = new JoinAggregateTransformNode(parent, {
                    joinaggregate: [
                        {
                            op,
                            field,
                            as: facetSortFieldName(fieldDef, fieldDef.sort, { forAs: true }),
                        },
                    ],
                    groupby: [vgField(fieldDef)],
                });
            }
        }
        return newParent;
    }
    return null;
}

function findSource(data, sources) {
    for (const other of sources) {
        const otherData = other.data;
        // if both datasets have a name defined, we cannot merge
        if (data.name && other.hasName() && data.name !== other.dataName) {
            continue;
        }
        const formatMesh = data.format?.mesh;
        const otherFeature = otherData.format?.feature;
        // feature and mesh are mutually exclusive
        if (formatMesh && otherFeature) {
            continue;
        }
        // we have to extract the same feature or mesh
        const formatFeature = data.format?.feature;
        if ((formatFeature || otherFeature) && formatFeature !== otherFeature) {
            continue;
        }
        const otherMesh = otherData.format?.mesh;
        if ((formatMesh || otherMesh) && formatMesh !== otherMesh) {
            continue;
        }
        if (isInlineData(data) && isInlineData(otherData)) {
            if (deepEqual(data.values, otherData.values)) {
                return other;
            }
        }
        else if (isUrlData(data) && isUrlData(otherData)) {
            if (data.url === otherData.url) {
                return other;
            }
        }
        else if (isNamedData(data)) {
            if (data.name === other.dataName) {
                return other;
            }
        }
    }
    return null;
}
function parseRoot(model, sources) {
    if (model.data || !model.parent) {
        // if the model defines a data source or is the root, create a source node
        if (model.data === null) {
            // data: null means we should ignore the parent's data so we just create a new data source
            const source = new SourceNode({ values: [] });
            sources.push(source);
            return source;
        }
        const existingSource = findSource(model.data, sources);
        if (existingSource) {
            if (!isGenerator(model.data)) {
                existingSource.data.format = mergeDeep$1({}, model.data.format, existingSource.data.format);
            }
            // if the new source has a name but the existing one does not, we can set it
            if (!existingSource.hasName() && model.data.name) {
                existingSource.dataName = model.data.name;
            }
            return existingSource;
        }
        else {
            const source = new SourceNode(model.data);
            sources.push(source);
            return source;
        }
    }
    else {
        // If we don't have a source defined (overriding parent's data), use the parent's facet root or main.
        return model.parent.component.data.facetRoot
            ? model.parent.component.data.facetRoot
            : model.parent.component.data.main;
    }
}
/**
 * Parses a transform array into a chain of connected dataflow nodes.
 */
function parseTransformArray(head, model, ancestorParse) {
    let lookupCounter = 0;
    for (const t of model.transforms) {
        let derivedType = undefined;
        let transformNode;
        if (isCalculate(t)) {
            transformNode = head = new CalculateNode(head, t);
            derivedType = 'derived';
        }
        else if (isFilter(t)) {
            const implicit = getImplicitFromFilterTransform(t);
            transformNode = head = ParseNode.makeWithAncestors(head, {}, implicit, ancestorParse) ?? head;
            head = new FilterNode(head, model, t.filter);
        }
        else if (isBin(t)) {
            transformNode = head = BinNode.makeFromTransform(head, t, model);
            derivedType = 'number';
        }
        else if (isTimeUnit(t)) {
            derivedType = 'date';
            const parsedAs = ancestorParse.getWithExplicit(t.field);
            // Create parse node because the input to time unit is always date.
            if (parsedAs.value === undefined) {
                head = new ParseNode(head, { [t.field]: derivedType });
                ancestorParse.set(t.field, derivedType, false);
            }
            transformNode = head = TimeUnitNode.makeFromTransform(head, t);
        }
        else if (isAggregate(t)) {
            transformNode = head = AggregateNode.makeFromTransform(head, t);
            derivedType = 'number';
            if (requiresSelectionId(model)) {
                head = new IdentifierNode(head);
            }
        }
        else if (isLookup(t)) {
            transformNode = head = LookupNode.make(head, model, t, lookupCounter++);
            derivedType = 'derived';
        }
        else if (isWindow(t)) {
            transformNode = head = new WindowTransformNode(head, t);
            derivedType = 'number';
        }
        else if (isJoinAggregate(t)) {
            transformNode = head = new JoinAggregateTransformNode(head, t);
            derivedType = 'number';
        }
        else if (isStack(t)) {
            transformNode = head = StackNode.makeFromTransform(head, t);
            derivedType = 'derived';
        }
        else if (isFold(t)) {
            transformNode = head = new FoldTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isExtent(t)) {
            transformNode = head = new ExtentTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isFlatten(t)) {
            transformNode = head = new FlattenTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isPivot(t)) {
            transformNode = head = new PivotTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isSample(t)) {
            head = new SampleTransformNode(head, t);
        }
        else if (isImpute(t)) {
            transformNode = head = ImputeNode.makeFromTransform(head, t);
            derivedType = 'derived';
        }
        else if (isDensity(t)) {
            transformNode = head = new DensityTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isQuantile(t)) {
            transformNode = head = new QuantileTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isRegression(t)) {
            transformNode = head = new RegressionTransformNode(head, t);
            derivedType = 'derived';
        }
        else if (isLoess(t)) {
            transformNode = head = new LoessTransformNode(head, t);
            derivedType = 'derived';
        }
        else {
            warn(invalidTransformIgnored(t));
            continue;
        }
        if (transformNode && derivedType !== undefined) {
            for (const field of transformNode.producedFields() ?? []) {
                ancestorParse.set(field, derivedType, false);
            }
        }
    }
    return head;
}
/*
Description of the dataflow (http://asciiflow.com/):
     +--------+
     | Source |
     +---+----+
         |
         v
     FormatParse
     (explicit)
         |
         v
     Transforms
(Filter, Calculate, Binning, TimeUnit, Aggregate, Window, ...)
         |
         v
     FormatParse
     (implicit)
         |
         v
 Binning (in `encoding`)
         |
         v
 Timeunit (in `encoding`)
         |
         v
Formula From Sort Array
         |
         v
      +--+--+
      | Raw |
      +-----+
         |
         v
  Aggregate (in `encoding`)
         |
         v
  Stack (in `encoding`)
         |
         v
+- - - - - - - - - - -+
|   PreFilterInvalid  | - - - -> scale domains
|(when scales need it)|
+- - - - - - - - - - -+
         |
         v
  Invalid Filter (if the main data source needs it)
         |
         v
   +----------+
   |   Main   | - - - -> scale domains
   +----------+
         |
         v
+- - - - - - - - - - -+
|   PostFilterInvalid | - - - -> scale domains
|(when scales need it)|
+- - - - - - - - - - -+
         |
         v
     +-------+
     | Facet |----> "column", "column-layout", and "row"
     +-------+
         |
         v
  ...Child data...
*/
function parseData(model) {
    let head = parseRoot(model, model.component.data.sources);
    const { outputNodes, outputNodeRefCounts } = model.component.data;
    const data = model.data;
    const newData = data && (isGenerator(data) || isUrlData(data) || isInlineData(data));
    const ancestorParse = !newData && model.parent ? model.parent.component.data.ancestorParse.clone() : new AncestorParse();
    if (isGenerator(data)) {
        // insert generator transform
        if (isSequenceGenerator(data)) {
            head = new SequenceNode(head, data.sequence);
        }
        else if (isGraticuleGenerator(data)) {
            head = new GraticuleNode(head, data.graticule);
        }
        // no parsing necessary for generator
        ancestorParse.parseNothing = true;
    }
    else if (data?.format?.parse === null) {
        // format.parse: null means disable parsing
        ancestorParse.parseNothing = true;
    }
    head = ParseNode.makeExplicit(head, model, ancestorParse) ?? head;
    // Default discrete selections require an identifer transform to
    // uniquely identify data points. Add this transform at the head of
    // the pipeline such that the identifier field is available for all
    // subsequent datasets. During optimization, we will remove this
    // transform if it proves to be unnecessary. Additional identifier
    // transforms will be necessary when new tuples are constructed
    // (e.g., post-aggregation).
    head = new IdentifierNode(head);
    // HACK: This is equivalent for merging bin extent for union scale.
    // FIXME(https://github.com/vega/vega-lite/issues/2270): Correctly merge extent / bin node for shared bin scale
    const parentIsLayer = model.parent && isLayerModel(model.parent);
    if (isUnitModel(model) || isFacetModel(model)) {
        if (parentIsLayer) {
            head = BinNode.makeFromEncoding(head, model) ?? head;
        }
    }
    if (model.transforms.length > 0) {
        head = parseTransformArray(head, model, ancestorParse);
    }
    // create parse nodes for fields that need to be parsed (or flattened) implicitly
    const implicitSelection = getImplicitFromSelection(model);
    const implicitEncoding = getImplicitFromEncoding(model);
    head = ParseNode.makeWithAncestors(head, {}, { ...implicitSelection, ...implicitEncoding }, ancestorParse) ?? head;
    if (isUnitModel(model)) {
        head = GeoJSONNode.parseAll(head, model);
        head = GeoPointNode.parseAll(head, model);
    }
    if (isUnitModel(model) || isFacetModel(model)) {
        if (!parentIsLayer) {
            head = BinNode.makeFromEncoding(head, model) ?? head;
        }
        head = TimeUnitNode.makeFromEncoding(head, model) ?? head;
        head = CalculateNode.parseAllForSortIndex(head, model);
    }
    // add an output node pre aggregation
    const raw = (head = makeOutputNode(DataSourceType.Raw, model, head));
    if (isUnitModel(model)) {
        const agg = AggregateNode.makeFromEncoding(head, model);
        if (agg) {
            head = agg;
            if (requiresSelectionId(model)) {
                head = new IdentifierNode(head);
            }
        }
        head = ImputeNode.makeFromEncoding(head, model) ?? head;
        head = StackNode.makeFromEncoding(head, model) ?? head;
    }
    let preFilterInvalid;
    let dataSourcesForHandlingInvalidValues;
    if (isUnitModel(model)) {
        const { markDef, mark, config } = model;
        const invalid = getMarkPropOrConfig('invalid', markDef, config);
        const { marks, scales } = (dataSourcesForHandlingInvalidValues = getDataSourcesForHandlingInvalidValues({
            invalid,
            isPath: isPathMark(mark),
        }));
        if (marks !== scales && scales === 'include-invalid-values') {
            // Create a seperate preFilterInvalid dataSource if scales need pre-filter data but marks needs post-filter.
            preFilterInvalid = head = makeOutputNode(DataSourceType.PreFilterInvalid, model, head);
        }
        if (marks === 'exclude-invalid-values') {
            head = FilterInvalidNode.make(head, model, dataSourcesForHandlingInvalidValues) ?? head;
        }
    }
    // output "main" node for marks
    const main = (head = makeOutputNode(DataSourceType.Main, model, head));
    let postFilterInvalid;
    if (isUnitModel(model) && dataSourcesForHandlingInvalidValues) {
        const { marks, scales } = dataSourcesForHandlingInvalidValues;
        if (marks === 'include-invalid-values' && scales === 'exclude-invalid-values') {
            // Create a seperate postFilterInvalid dataSource if scales need post-filter data but marks needs pre-filter.
            head = FilterInvalidNode.make(head, model, dataSourcesForHandlingInvalidValues) ?? head;
            postFilterInvalid = head = makeOutputNode(DataSourceType.PostFilterInvalid, model, head);
        }
    }
    if (isUnitModel(model)) {
        materializeSelections(model, main);
    }
    // add facet marker
    let facetRoot = null;
    if (isFacetModel(model)) {
        const facetName = model.getName('facet');
        // Derive new aggregate for facet's sort field
        // augment data source with new fields for crossed facet
        head = makeJoinAggregateFromFacet(head, model.facet) ?? head;
        facetRoot = new FacetNode(head, model, facetName, main.getSource());
        outputNodes[facetName] = facetRoot;
    }
    return {
        ...model.component.data,
        outputNodes,
        outputNodeRefCounts,
        raw,
        main,
        facetRoot,
        ancestorParse,
        preFilterInvalid,
        postFilterInvalid,
    };
}
function makeOutputNode(dataSourceType, model, head) {
    const { outputNodes, outputNodeRefCounts } = model.component.data;
    const name = model.getDataName(dataSourceType);
    const node = new OutputNode(head, name, dataSourceType, outputNodeRefCounts);
    outputNodes[name] = node;
    return node;
}

class ConcatModel extends Model {
    children;
    constructor(spec, parent, parentGivenName, config) {
        super(spec, 'concat', parent, parentGivenName, config, spec.resolve);
        if (spec.resolve?.axis?.x === 'shared' || spec.resolve?.axis?.y === 'shared') {
            warn(CONCAT_CANNOT_SHARE_AXIS);
        }
        this.children = this.getChildren(spec).map((child, i) => {
            return buildModel(child, this, this.getName(`concat_${i}`), undefined, config);
        });
    }
    parseData() {
        this.component.data = parseData(this);
        for (const child of this.children) {
            child.parseData();
        }
    }
    parseSelections() {
        // Merge selections up the hierarchy so that they may be referenced
        // across unit specs. Persist their definitions within each child
        // to assemble signals which remain within output Vega unit groups.
        this.component.selection = {};
        for (const child of this.children) {
            child.parseSelections();
            for (const key of keys(child.component.selection)) {
                this.component.selection[key] = child.component.selection[key];
            }
        }
        if (vals(this.component.selection).some((selCmpt) => isTimerSelection(selCmpt))) {
            error(MULTI_VIEW_ANIMATION_UNSUPPORTED);
        }
    }
    parseMarkGroup() {
        for (const child of this.children) {
            child.parseMarkGroup();
        }
    }
    parseAxesAndHeaders() {
        for (const child of this.children) {
            child.parseAxesAndHeaders();
        }
        // TODO(#2415): support shared axes
    }
    getChildren(spec) {
        if (isVConcatSpec(spec)) {
            return spec.vconcat;
        }
        else if (isHConcatSpec(spec)) {
            return spec.hconcat;
        }
        return spec.concat;
    }
    parseLayoutSize() {
        parseConcatLayoutSize(this);
    }
    parseAxisGroup() {
        return null;
    }
    assembleSelectionTopLevelSignals(signals) {
        return this.children.reduce((sg, child) => child.assembleSelectionTopLevelSignals(sg), signals);
    }
    assembleSignals() {
        this.children.forEach((child) => child.assembleSignals());
        return [];
    }
    assembleLayoutSignals() {
        const layoutSignals = assembleLayoutSignals(this);
        for (const child of this.children) {
            layoutSignals.push(...child.assembleLayoutSignals());
        }
        return layoutSignals;
    }
    assembleSelectionData(data) {
        return this.children.reduce((db, child) => child.assembleSelectionData(db), data);
    }
    assembleMarks() {
        // only children have marks
        return this.children.map((child) => {
            const title = child.assembleTitle();
            const style = child.assembleGroupStyle();
            const encodeEntry = child.assembleGroupEncodeEntry(false);
            return {
                type: 'group',
                name: child.getName('group'),
                ...(title ? { title } : {}),
                ...(style ? { style } : {}),
                ...(encodeEntry ? { encode: { update: encodeEntry } } : {}),
                ...child.assembleGroup(),
            };
        });
    }
    assembleGroupStyle() {
        return undefined;
    }
    assembleDefaultLayout() {
        const columns = this.layout.columns;
        return {
            ...(columns != null ? { columns } : {}),
            bounds: 'full',
            // Use align each so it can work with multiple plots with different size
            align: 'each',
        };
    }
}

function isFalseOrNull(v) {
    return v === false || v === null;
}
const AXIS_COMPONENT_PROPERTIES_INDEX = {
    disable: 1,
    gridScale: 1,
    scale: 1,
    ...COMMON_AXIS_PROPERTIES_INDEX,
    labelExpr: 1,
    encode: 1,
};
const AXIS_COMPONENT_PROPERTIES = keys(AXIS_COMPONENT_PROPERTIES_INDEX);
class AxisComponent extends Split {
    explicit;
    implicit;
    mainExtracted;
    constructor(explicit = {}, implicit = {}, mainExtracted = false) {
        super();
        this.explicit = explicit;
        this.implicit = implicit;
        this.mainExtracted = mainExtracted;
    }
    clone() {
        return new AxisComponent(duplicate(this.explicit), duplicate(this.implicit), this.mainExtracted);
    }
    hasAxisPart(part) {
        // FIXME(https://github.com/vega/vega-lite/issues/2552) this method can be wrong if users use a Vega theme.
        if (part === 'axis') {
            // always has the axis container part
            return true;
        }
        if (part === 'grid' || part === 'title') {
            return !!this.get(part);
        }
        // Other parts are enabled by default, so they should not be false or null.
        return !isFalseOrNull(this.get(part));
    }
    hasOrientSignalRef() {
        return isSignalRef(this.explicit.orient);
    }
}

function labels(model, channel, specifiedLabelsSpec) {
    const { encoding, config } = model;
    const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]) ?? getFieldOrDatumDef(encoding[getSecondaryRangeChannel(channel)]);
    const axis = model.axis(channel) || {};
    const { format, formatType } = axis;
    if (isCustomFormatType(formatType)) {
        return {
            text: formatCustomType({
                fieldOrDatumDef,
                field: 'datum.value',
                format,
                formatType,
                config,
            }),
            ...specifiedLabelsSpec,
        };
    }
    else if (format === undefined && formatType === undefined && config.customFormatTypes) {
        if (channelDefType(fieldOrDatumDef) === 'quantitative') {
            if (isPositionFieldOrDatumDef(fieldOrDatumDef) &&
                fieldOrDatumDef.stack === 'normalize' &&
                config.normalizedNumberFormatType) {
                return {
                    text: formatCustomType({
                        fieldOrDatumDef,
                        field: 'datum.value',
                        format: config.normalizedNumberFormat,
                        formatType: config.normalizedNumberFormatType,
                        config,
                    }),
                    ...specifiedLabelsSpec,
                };
            }
            else if (config.numberFormatType) {
                return {
                    text: formatCustomType({
                        fieldOrDatumDef,
                        field: 'datum.value',
                        format: config.numberFormat,
                        formatType: config.numberFormatType,
                        config,
                    }),
                    ...specifiedLabelsSpec,
                };
            }
        }
        if (channelDefType(fieldOrDatumDef) === 'temporal' &&
            config.timeFormatType &&
            isFieldDef(fieldOrDatumDef) &&
            !fieldOrDatumDef.timeUnit) {
            return {
                text: formatCustomType({
                    fieldOrDatumDef,
                    field: 'datum.value',
                    format: config.timeFormat,
                    formatType: config.timeFormatType,
                    config,
                }),
                ...specifiedLabelsSpec,
            };
        }
    }
    return specifiedLabelsSpec;
}

function parseUnitAxes(model) {
    return POSITION_SCALE_CHANNELS.reduce((axis, channel) => {
        if (model.component.scales[channel]) {
            axis[channel] = [parseAxis(channel, model)];
        }
        return axis;
    }, {});
}
const OPPOSITE_ORIENT = {
    bottom: 'top',
    top: 'bottom',
    left: 'right',
    right: 'left',
};
function parseLayerAxes(model) {
    const { axes, resolve } = model.component;
    const axisCount = { top: 0, bottom: 0, right: 0, left: 0 };
    for (const child of model.children) {
        child.parseAxesAndHeaders();
        for (const channel of keys(child.component.axes)) {
            resolve.axis[channel] = parseGuideResolve(model.component.resolve, channel);
            if (resolve.axis[channel] === 'shared') {
                // If the resolve says shared (and has not been overridden)
                // We will try to merge and see if there is a conflict
                axes[channel] = mergeAxisComponents(axes[channel], child.component.axes[channel]);
                if (!axes[channel]) {
                    // If merge returns nothing, there is a conflict so we cannot make the axis shared.
                    // Thus, mark axis as independent and remove the axis component.
                    resolve.axis[channel] = 'independent';
                    delete axes[channel];
                }
            }
        }
    }
    // Move axes to layer's axis component and merge shared axes
    for (const channel of POSITION_SCALE_CHANNELS) {
        for (const child of model.children) {
            if (!child.component.axes[channel]) {
                // skip if the child does not have a particular axis
                continue;
            }
            if (resolve.axis[channel] === 'independent') {
                // If axes are independent, concat the axisComponent array.
                axes[channel] = (axes[channel] ?? []).concat(child.component.axes[channel]);
                // Automatically adjust orient
                for (const axisComponent of child.component.axes[channel]) {
                    const { value: orient, explicit } = axisComponent.getWithExplicit('orient');
                    if (isSignalRef(orient)) {
                        continue;
                    }
                    if (axisCount[orient] > 0 && !explicit) {
                        // Change axis orient if the number do not match
                        const oppositeOrient = OPPOSITE_ORIENT[orient];
                        if (axisCount[orient] > axisCount[oppositeOrient]) {
                            axisComponent.set('orient', oppositeOrient, false);
                        }
                    }
                    axisCount[orient]++;
                    // TODO(https://github.com/vega/vega-lite/issues/2634): automatically add extra offset?
                }
            }
            // After merging, make sure to remove axes from child
            delete child.component.axes[channel];
        }
        // Show gridlines for first axis only for dual-axis chart
        if (resolve.axis[channel] === 'independent' && axes[channel] && axes[channel].length > 1) {
            for (const [index, axisCmpt] of (axes[channel] || []).entries()) {
                if (index > 0 && !!axisCmpt.get('grid') && !axisCmpt.explicit.grid) {
                    axisCmpt.implicit.grid = false;
                }
            }
        }
    }
}
function mergeAxisComponents(mergedAxisCmpts, childAxisCmpts) {
    if (mergedAxisCmpts) {
        // FIXME: this is a bit wrong once we support multiple axes
        if (mergedAxisCmpts.length !== childAxisCmpts.length) {
            return undefined; // Cannot merge axis component with different number of axes.
        }
        const length = mergedAxisCmpts.length;
        for (let i = 0; i < length; i++) {
            const merged = mergedAxisCmpts[i];
            const child = childAxisCmpts[i];
            if (!!merged !== !!child) {
                return undefined;
            }
            else if (merged && child) {
                const mergedOrient = merged.getWithExplicit('orient');
                const childOrient = child.getWithExplicit('orient');
                if (mergedOrient.explicit && childOrient.explicit && mergedOrient.value !== childOrient.value) {
                    // TODO: throw warning if resolve is explicit (We don't have info about explicit/implicit resolve yet.)
                    // Cannot merge due to inconsistent orient
                    return undefined;
                }
                else {
                    mergedAxisCmpts[i] = mergeAxisComponent(merged, child);
                }
            }
        }
    }
    else {
        // For first one, return a copy of the child
        return childAxisCmpts.map((axisComponent) => axisComponent.clone());
    }
    return mergedAxisCmpts;
}
function mergeAxisComponent(merged, child) {
    for (const prop of AXIS_COMPONENT_PROPERTIES) {
        const mergedValueWithExplicit = mergeValuesWithExplicit(merged.getWithExplicit(prop), child.getWithExplicit(prop), prop, 'axis', 
        // Tie breaker function
        (v1, v2) => {
            switch (prop) {
                case 'title':
                    return mergeTitleComponent(v1, v2);
                case 'gridScale':
                    return {
                        explicit: v1.explicit, // keep the old explicit
                        value: getFirstDefined(v1.value, v2.value),
                    };
            }
            return defaultTieBreaker(v1, v2, prop, 'axis');
        });
        merged.setWithExplicit(prop, mergedValueWithExplicit);
    }
    return merged;
}
function isExplicit(value, property, axis, model, channel) {
    if (property === 'disable') {
        return axis !== undefined; // if axis is specified or null/false, then its enable/disable state is explicit
    }
    axis = axis || {};
    switch (property) {
        case 'titleAngle':
        case 'labelAngle':
            return value === (isSignalRef(axis.labelAngle) ? axis.labelAngle : normalizeAngle(axis.labelAngle));
        case 'values':
            return !!axis.values;
        // specified axis.values is already respected, but may get transformed.
        case 'encode':
            // both VL axis.encoding and axis.labelAngle affect VG axis.encode
            return !!axis.encoding || !!axis.labelAngle;
        case 'title':
            // title can be explicit if fieldDef.title is set
            if (value === getFieldDefTitle(model, channel)) {
                return true;
            }
    }
    // Otherwise, things are explicit if the returned value matches the specified property
    return value === axis[property];
}
/**
 * Properties to always include values from config
 */
const propsToAlwaysIncludeConfig = new Set([
    'grid', // Grid is an exception because we need to set grid = true to generate another grid axis
    'translate', // translate has dependent logic for bar's bin position and it's 0.5 by default in Vega. If a config overrides this value, we need to know.
    // the rest are not axis configs in Vega, but are in VL, so we need to set too.
    'format',
    'formatType',
    'orient',
    'labelExpr',
    'tickCount',
    'position',
    'tickMinStep',
]);
function parseAxis(channel, model) {
    let axis = model.axis(channel);
    const axisComponent = new AxisComponent();
    const fieldOrDatumDef = getFieldOrDatumDef(model.encoding[channel]);
    const { mark, config } = model;
    const orient = axis?.orient ||
        config[channel === 'x' ? 'axisX' : 'axisY']?.orient ||
        config.axis?.orient ||
        defaultOrient(channel);
    const scaleType = model.getScaleComponent(channel).get('type');
    const axisConfigs = getAxisConfigs(channel, scaleType, orient, model.config);
    const disable = axis !== undefined ? !axis : getAxisConfig('disable', config.style, axis?.style, axisConfigs).configValue;
    axisComponent.set('disable', disable, axis !== undefined);
    if (disable) {
        return axisComponent;
    }
    axis = axis || {};
    const labelAngle = getLabelAngle(fieldOrDatumDef, axis, channel, config.style, axisConfigs);
    const formatType = guideFormatType(axis.formatType, fieldOrDatumDef, scaleType);
    const format = guideFormat(fieldOrDatumDef, fieldOrDatumDef.type, axis.format, axis.formatType, config, true);
    const ruleParams = {
        fieldOrDatumDef,
        axis,
        channel,
        model,
        scaleType,
        orient,
        labelAngle,
        format,
        formatType,
        mark,
        config,
    };
    // 1.2. Add properties
    for (const property of AXIS_COMPONENT_PROPERTIES) {
        const value = property in axisRules ? axisRules[property](ruleParams) : isAxisProperty(property) ? axis[property] : undefined;
        const hasValue = value !== undefined;
        const explicit = isExplicit(value, property, axis, model, channel);
        if (hasValue && explicit) {
            axisComponent.set(property, value, explicit);
        }
        else {
            const { configValue = undefined, configFrom = undefined } = isAxisProperty(property) && property !== 'values'
                ? getAxisConfig(property, config.style, axis.style, axisConfigs)
                : {};
            const hasConfigValue = configValue !== undefined;
            if (hasValue && !hasConfigValue) {
                // only set property if it is explicitly set or has no config value (otherwise we will accidentally override config)
                axisComponent.set(property, value, explicit);
            }
            else if (
            // Cases need implicit values
            // 1. Axis config that aren't available in Vega
            !(configFrom === 'vgAxisConfig') ||
                // 2. Certain properties are always included (see `propsToAlwaysIncludeConfig`'s declaration for more details)
                (propsToAlwaysIncludeConfig.has(property) && hasConfigValue) ||
                // 3. Conditional axis values and signals
                isConditionalAxisValue(configValue) ||
                isSignalRef(configValue)) {
                // If a config is specified and is conditional, copy conditional value from axis config
                axisComponent.set(property, configValue, false);
            }
        }
    }
    // 2) Add guide encode definition groups
    const axisEncoding = axis.encoding ?? {};
    const axisEncode = AXIS_PARTS.reduce((e, part) => {
        if (!axisComponent.hasAxisPart(part)) {
            // No need to create encode for a disabled part.
            return e;
        }
        const axisEncodingPart = guideEncodeEntry(axisEncoding[part] ?? {}, model);
        const value = part === 'labels' ? labels(model, channel, axisEncodingPart) : axisEncodingPart;
        if (value !== undefined && !isEmpty(value)) {
            e[part] = { update: value };
        }
        return e;
    }, {});
    // FIXME: By having encode as one property, we won't have fine grained encode merging.
    if (!isEmpty(axisEncode)) {
        axisComponent.set('encode', axisEncode, !!axis.encoding || axis.labelAngle !== undefined);
    }
    return axisComponent;
}

function initLayoutSize({ encoding, size }) {
    for (const channel of POSITION_SCALE_CHANNELS) {
        const sizeType = getSizeChannel(channel);
        if (isStep(size[sizeType])) {
            if (isContinuousFieldOrDatumDef(encoding[channel])) {
                delete size[sizeType];
                warn(stepDropped(sizeType));
            }
        }
    }
    return size;
}

const arc = {
    vgMark: 'arc',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                size: 'ignore',
                orient: 'ignore',
                theta: 'ignore',
            }),
            ...pointPosition('x', model, { defaultPos: 'mid' }),
            ...pointPosition('y', model, { defaultPos: 'mid' }),
            // arcs are rectangles in polar coordinates
            ...rectPosition(model, 'radius'),
            ...rectPosition(model, 'theta'),
        };
    },
};

const area = {
    vgMark: 'area',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                orient: 'include',
                size: 'ignore',
                theta: 'ignore',
            }),
            ...pointOrRangePosition('x', model, {
                defaultPos: 'zeroOrMin',
                defaultPos2: 'zeroOrMin',
                range: model.markDef.orient === 'horizontal',
            }),
            ...pointOrRangePosition('y', model, {
                defaultPos: 'zeroOrMin',
                defaultPos2: 'zeroOrMin',
                range: model.markDef.orient === 'vertical',
            }),
            ...defined(model),
        };
    },
};

const bar = {
    vgMark: 'rect',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                orient: 'ignore',
                size: 'ignore',
                theta: 'ignore',
            }),
            ...rectPosition(model, 'x'),
            ...rectPosition(model, 'y'),
        };
    },
};

const geoshape = {
    vgMark: 'shape',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                size: 'ignore',
                orient: 'ignore',
                theta: 'ignore',
            }),
        };
    },
    postEncodingTransform: (model) => {
        const { encoding } = model;
        const shapeDef = encoding.shape;
        const transform = {
            type: 'geoshape',
            projection: model.projectionName(),
            // as: 'shape',
            ...(shapeDef && isFieldDef(shapeDef) && shapeDef.type === GEOJSON
                ? { field: vgField(shapeDef, { expr: 'datum' }) }
                : {}),
        };
        return [transform];
    },
};

const image = {
    vgMark: 'image',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'ignore',
                orient: 'ignore',
                size: 'ignore',
                theta: 'ignore',
            }),
            ...rectPosition(model, 'x'),
            ...rectPosition(model, 'y'),
            ...text$1(model, 'url'),
        };
    },
};

const line = {
    vgMark: 'line',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                size: 'ignore',
                orient: 'ignore',
                theta: 'ignore',
            }),
            ...pointPosition('x', model, { defaultPos: 'mid' }),
            ...pointPosition('y', model, { defaultPos: 'mid' }),
            ...nonPosition('size', model, {
                vgChannel: 'strokeWidth', // VL's line size is strokeWidth
            }),
            ...defined(model),
        };
    },
};
const trail = {
    vgMark: 'trail',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                size: 'include',
                orient: 'ignore',
                theta: 'ignore',
            }),
            ...pointPosition('x', model, { defaultPos: 'mid' }),
            ...pointPosition('y', model, { defaultPos: 'mid' }),
            ...nonPosition('size', model),
            ...defined(model),
        };
    },
};

function encodeEntry(model, fixedShape) {
    const { config } = model;
    return {
        ...baseEncodeEntry(model, {
            align: 'ignore',
            baseline: 'ignore',
            color: 'include',
            size: 'include',
            orient: 'ignore',
            theta: 'ignore',
        }),
        ...pointPosition('x', model, { defaultPos: 'mid' }),
        ...pointPosition('y', model, { defaultPos: 'mid' }),
        ...nonPosition('size', model),
        ...nonPosition('angle', model),
        ...shapeMixins(model, config, fixedShape),
    };
}
function shapeMixins(model, config, fixedShape) {
    if (fixedShape) {
        return { shape: { value: fixedShape } };
    }
    return nonPosition('shape', model);
}
const point = {
    vgMark: 'symbol',
    encodeEntry: (model) => {
        return encodeEntry(model);
    },
};
const circle = {
    vgMark: 'symbol',
    encodeEntry: (model) => {
        return encodeEntry(model, 'circle');
    },
};
const square = {
    vgMark: 'symbol',
    encodeEntry: (model) => {
        return encodeEntry(model, 'square');
    },
};

const rect = {
    vgMark: 'rect',
    encodeEntry: (model) => {
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                orient: 'ignore',
                size: 'ignore',
                theta: 'ignore',
            }),
            ...rectPosition(model, 'x'),
            ...rectPosition(model, 'y'),
        };
    },
};

const rule = {
    vgMark: 'rule',
    encodeEntry: (model) => {
        const { markDef } = model;
        const orient = markDef.orient;
        if (!model.encoding.x && !model.encoding.y && !model.encoding.latitude && !model.encoding.longitude) {
            // Show nothing if we have none of x, y, lat, and long.
            return {};
        }
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                orient: 'ignore',
                size: 'ignore',
                theta: 'ignore',
            }),
            ...pointOrRangePosition('x', model, {
                defaultPos: orient === 'horizontal' ? 'zeroOrMax' : 'mid',
                defaultPos2: 'zeroOrMin',
                range: orient !== 'vertical', // include x2 for horizontal or line segment rule
            }),
            ...pointOrRangePosition('y', model, {
                defaultPos: orient === 'vertical' ? 'zeroOrMax' : 'mid',
                defaultPos2: 'zeroOrMin',
                range: orient !== 'horizontal', // include y2 for vertical or line segment rule
            }),
            ...nonPosition('size', model, {
                vgChannel: 'strokeWidth', // VL's rule size is strokeWidth
            }),
        };
    },
};

const text = {
    vgMark: 'text',
    encodeEntry: (model) => {
        const { config, encoding } = model;
        return {
            ...baseEncodeEntry(model, {
                align: 'include',
                baseline: 'include',
                color: 'include',
                size: 'ignore',
                orient: 'ignore',
                theta: 'include',
            }),
            ...pointPosition('x', model, { defaultPos: 'mid' }),
            ...pointPosition('y', model, { defaultPos: 'mid' }),
            ...text$1(model),
            ...nonPosition('size', model, {
                vgChannel: 'fontSize', // VL's text size is fontSize
            }),
            ...nonPosition('angle', model),
            ...valueIfDefined('align', align(model.markDef, encoding, config)),
            ...valueIfDefined('baseline', baseline(model.markDef, encoding, config)),
            ...pointPosition('radius', model, { defaultPos: null }),
            ...pointPosition('theta', model, { defaultPos: null }),
        };
    },
};
function align(markDef, encoding, config) {
    const a = getMarkPropOrConfig('align', markDef, config);
    if (a === undefined) {
        return 'center';
    }
    // If there is a config, Vega-parser will process this already.
    return undefined;
}
function baseline(markDef, encoding, config) {
    const b = getMarkPropOrConfig('baseline', markDef, config);
    if (b === undefined) {
        return 'middle';
    }
    // If there is a config, Vega-parser will process this already.
    return undefined;
}

const tick = {
    vgMark: 'rect',
    encodeEntry: (model) => {
        const { config, markDef } = model;
        const orient = markDef.orient;
        const vgSizeAxisChannel = orient === 'horizontal' ? 'x' : 'y';
        const vgThicknessAxisChannel = orient === 'horizontal' ? 'y' : 'x';
        const vgThicknessChannel = orient === 'horizontal' ? 'height' : 'width';
        return {
            ...baseEncodeEntry(model, {
                align: 'ignore',
                baseline: 'ignore',
                color: 'include',
                orient: 'ignore',
                size: 'ignore',
                theta: 'ignore',
            }),
            ...rectPosition(model, vgSizeAxisChannel),
            ...pointPosition(vgThicknessAxisChannel, model, {
                defaultPos: 'mid',
                vgChannel: vgThicknessAxisChannel === 'y' ? 'yc' : 'xc',
            }),
            [vgThicknessChannel]: signalOrValueRef(getMarkPropOrConfig('thickness', markDef, config)),
        };
    },
};

const markCompiler = {
    arc,
    area,
    bar,
    circle,
    geoshape,
    image,
    line,
    point,
    rect,
    rule,
    square,
    text,
    tick,
    trail,
};
function parseMarkGroups(model) {
    if (contains([LINE, AREA, TRAIL], model.mark)) {
        const details = pathGroupingFields(model.mark, model.encoding);
        if (details.length > 0) {
            return getPathGroups(model, details);
        }
        // otherwise use standard mark groups
    }
    else if (model.mark === BAR) {
        const hasCornerRadius = VG_CORNERRADIUS_CHANNELS.some((prop) => getMarkPropOrConfig(prop, model.markDef, model.config));
        if (model.stack && !model.fieldDef('size') && hasCornerRadius) {
            return getGroupsForStackedBarWithCornerRadius(model);
        }
    }
    return getMarkGroup(model);
}
const FACETED_PATH_PREFIX = 'faceted_path_';
function getPathGroups(model, details) {
    // TODO: for non-stacked plot, map order to zindex. (Maybe rename order for layer to zindex?)
    return [
        {
            name: model.getName('pathgroup'),
            type: 'group',
            from: {
                facet: {
                    name: FACETED_PATH_PREFIX + model.requestDataName(DataSourceType.Main),
                    data: model.requestDataName(DataSourceType.Main),
                    groupby: details,
                },
            },
            encode: {
                update: {
                    width: { field: { group: 'width' } },
                    height: { field: { group: 'height' } },
                },
            },
            // With subfacet for line/area group, need to use faceted data from above.
            marks: getMarkGroup(model, { fromPrefix: FACETED_PATH_PREFIX }),
        },
    ];
}
const STACK_GROUP_PREFIX = 'stack_group_';
/**
 * We need to put stacked bars into groups in order to enable cornerRadius for stacks.
 * If stack is used and the model doesn't have size encoding, we put the mark into groups,
 * and apply cornerRadius properties at the group.
 */
function getGroupsForStackedBarWithCornerRadius(model) {
    // Generate the mark
    const [mark] = getMarkGroup(model, { fromPrefix: STACK_GROUP_PREFIX });
    // Get the scale for the stacked field
    const fieldScale = model.scaleName(model.stack.fieldChannel);
    const stackField = (opt = {}) => model.vgField(model.stack.fieldChannel, opt);
    // Find the min/max of the pixel value on the stacked direction
    const stackFieldGroup = (func, expr) => {
        const vgFieldMinMax = [
            stackField({ prefix: 'min', suffix: 'start', expr }),
            stackField({ prefix: 'max', suffix: 'start', expr }),
            stackField({ prefix: 'min', suffix: 'end', expr }),
            stackField({ prefix: 'max', suffix: 'end', expr }),
        ];
        return `${func}(${vgFieldMinMax.map((field) => `scale('${fieldScale}',${field})`).join(',')})`;
    };
    let groupUpdate;
    let innerGroupUpdate;
    // Build the encoding for group and an inner group
    if (model.stack.fieldChannel === 'x') {
        // Move cornerRadius, y/yc/y2/height properties to group
        // Group x/x2 should be the min/max of the marks within
        groupUpdate = {
            ...pick(mark.encode.update, ['y', 'yc', 'y2', 'height', ...VG_CORNERRADIUS_CHANNELS]),
            x: { signal: stackFieldGroup('min', 'datum') },
            x2: { signal: stackFieldGroup('max', 'datum') },
            clip: { value: true },
        };
        // Inner group should revert the x translation, and pass height through
        innerGroupUpdate = {
            x: { field: { group: 'x' }, mult: -1 },
            height: { field: { group: 'height' } },
        };
        // The marks should use the same height as group, without y/yc/y2 properties (because it's already done by group)
        // This is why size encoding is not supported yet
        mark.encode.update = {
            ...omit(mark.encode.update, ['y', 'yc', 'y2']),
            height: { field: { group: 'height' } },
        };
    }
    else {
        groupUpdate = {
            ...pick(mark.encode.update, ['x', 'xc', 'x2', 'width']),
            y: { signal: stackFieldGroup('min', 'datum') },
            y2: { signal: stackFieldGroup('max', 'datum') },
            clip: { value: true },
        };
        innerGroupUpdate = {
            y: { field: { group: 'y' }, mult: -1 },
            width: { field: { group: 'width' } },
        };
        mark.encode.update = {
            ...omit(mark.encode.update, ['x', 'xc', 'x2']),
            width: { field: { group: 'width' } },
        };
    }
    // Deal with cornerRadius properties
    for (const key of VG_CORNERRADIUS_CHANNELS) {
        const configValue = getMarkConfig(key, model.markDef, model.config);
        // Move from mark to group
        if (mark.encode.update[key]) {
            groupUpdate[key] = mark.encode.update[key];
            delete mark.encode.update[key];
        }
        else if (configValue) {
            groupUpdate[key] = signalOrValueRef(configValue);
        }
        // Overwrite any cornerRadius on mark set by config --- they are already moved to the group
        if (configValue) {
            mark.encode.update[key] = { value: 0 };
        }
    }
    const groupby = [];
    if (model.stack.groupbyChannels?.length > 0) {
        for (const groupbyChannel of model.stack.groupbyChannels) {
            // For bin and time unit, we have to add bin/timeunit -end channels.
            const groupByField = model.fieldDef(groupbyChannel);
            const field = vgField(groupByField);
            if (field) {
                groupby.push(field);
            }
            if (groupByField?.bin || groupByField?.timeUnit) {
                groupby.push(vgField(groupByField, { binSuffix: 'end' }));
            }
        }
    }
    const strokeProperties = [
        'stroke',
        'strokeWidth',
        'strokeJoin',
        'strokeCap',
        'strokeDash',
        'strokeDashOffset',
        'strokeMiterLimit',
        'strokeOpacity',
    ];
    // Generate stroke properties for the group
    groupUpdate = strokeProperties.reduce((encode, prop) => {
        if (mark.encode.update[prop]) {
            return { ...encode, [prop]: mark.encode.update[prop] };
        }
        else {
            const configValue = getMarkConfig(prop, model.markDef, model.config);
            if (configValue !== undefined) {
                return { ...encode, [prop]: signalOrValueRef(configValue) };
            }
            else {
                return encode;
            }
        }
    }, groupUpdate);
    // Apply strokeForeground and strokeOffset if stroke is used
    if (groupUpdate.stroke) {
        groupUpdate.strokeForeground = { value: true };
        groupUpdate.strokeOffset = { value: 0 };
    }
    return [
        {
            type: 'group',
            from: {
                facet: {
                    data: model.requestDataName(DataSourceType.Main),
                    name: STACK_GROUP_PREFIX + model.requestDataName(DataSourceType.Main),
                    groupby,
                    aggregate: {
                        fields: [
                            stackField({ suffix: 'start' }),
                            stackField({ suffix: 'start' }),
                            stackField({ suffix: 'end' }),
                            stackField({ suffix: 'end' }),
                        ],
                        ops: ['min', 'max', 'min', 'max'],
                    },
                },
            },
            encode: {
                update: groupUpdate,
            },
            marks: [
                {
                    type: 'group',
                    encode: { update: innerGroupUpdate },
                    marks: [mark],
                },
            ],
        },
    ];
}
function getSort(model) {
    const { encoding, stack, mark, markDef, config } = model;
    const order = encoding.order;
    if ((!isArray(order) && isValueDef(order) && isNullOrFalse(order.value)) ||
        (!order && isNullOrFalse(getMarkPropOrConfig('order', markDef, config)))) {
        return undefined;
    }
    else if ((isArray(order) || isFieldDef(order)) && !stack) {
        // Sort by the order field if it is specified and the field is not stacked. (For stacked field, order specify stack order.)
        return sortParams(order, { expr: 'datum' });
    }
    else if (isPathMark(mark)) {
        // For both line and area, we sort values based on dimension by default
        const dimensionChannel = markDef.orient === 'horizontal' ? 'y' : 'x';
        const dimensionChannelDef = encoding[dimensionChannel];
        if (isFieldDef(dimensionChannelDef)) {
            return { field: dimensionChannel };
        }
    }
    return undefined;
}
function getMarkGroup(model, opt = { fromPrefix: '' }) {
    const { mark, markDef, encoding, config } = model;
    const clip = getFirstDefined(markDef.clip, scaleClip(model), projectionClip(model));
    const style = getStyles(markDef);
    const key = encoding.key;
    const sort = getSort(model);
    const interactive = interactiveFlag(model);
    // set pointer cursor for point selections that are not bound
    if (interactive &&
        Object.values(model.component.selection).some((s) => s.type === 'point' &&
            !s.bind &&
            // if on is a pointerover (hover) the pointer makes less sense since the mark is not clickable.
            s.on !== 'pointerover')) {
        model.markDef.cursor ??= 'pointer';
    }
    const aria = getMarkPropOrConfig('aria', markDef, config);
    const postEncodingTransform = markCompiler[mark].postEncodingTransform
        ? markCompiler[mark].postEncodingTransform(model)
        : null;
    return [
        {
            name: model.getName('marks'),
            type: markCompiler[mark].vgMark,
            ...(clip ? { clip } : {}),
            ...(style ? { style } : {}),
            ...(key ? { key: key.field } : {}),
            ...(sort ? { sort } : {}),
            ...(interactive ? interactive : {}),
            ...(aria === false ? { aria } : {}),
            from: { data: opt.fromPrefix + model.requestDataName(DataSourceType.Main) },
            encode: {
                update: markCompiler[mark].encodeEntry(model),
            },
            ...(postEncodingTransform
                ? {
                    transform: postEncodingTransform,
                }
                : {}),
        },
    ];
}
/**
 * If scales are bound to interval selections, we want to automatically clip
 * marks to account for panning/zooming interactions. We identify bound scales
 * by the selectionExtent property, which gets added during scale parsing.
 */
function scaleClip(model) {
    const xScale = model.getScaleComponent('x');
    const yScale = model.getScaleComponent('y');
    return xScale?.get('selectionExtent') || yScale?.get('selectionExtent') ? true : undefined;
}
/**
 * If we use a custom projection with auto-fitting to the geodata extent,
 * we need to clip to ensure the chart size doesn't explode.
 */
function projectionClip(model) {
    const projection = model.component.projection;
    return projection && !projection.isFit ? true : undefined;
}
/**
 * Only output interactive flags if we have selections defined somewhere in our model hierarchy.
 */
function interactiveFlag(model) {
    if (!model.component.selection)
        return null;
    const unitCount = keys(model.component.selection).length;
    let parentCount = unitCount;
    let parent = model.parent;
    while (parent && parentCount === 0) {
        parentCount = keys(parent.component.selection).length;
        parent = parent.parent;
    }
    return parentCount
        ? {
            interactive: unitCount > 0 || model.mark === 'geoshape' || !!model.encoding.tooltip || !!model.markDef.tooltip,
        }
        : null;
}

/**
 * Internal model of Vega-Lite specification for the compiler.
 */
class UnitModel extends ModelWithField {
    markDef;
    encoding;
    specifiedScales = {};
    stack;
    specifiedAxes = {};
    specifiedLegends = {};
    specifiedProjection = {};
    selection = [];
    children = [];
    constructor(spec, parent, parentGivenName, parentGivenSize = {}, config) {
        super(spec, 'unit', parent, parentGivenName, config, undefined, isFrameMixins(spec) ? spec.view : undefined);
        const markDef = isMarkDef(spec.mark) ? { ...spec.mark } : { type: spec.mark };
        const mark = markDef.type;
        // Need to init filled before other mark properties because encoding depends on filled but other mark properties depend on types inside encoding
        if (markDef.filled === undefined) {
            markDef.filled = defaultFilled(markDef, config, {
                graticule: spec.data && isGraticuleGenerator(spec.data),
            });
        }
        const encoding = (this.encoding = initEncoding(spec.encoding || {}, mark, markDef.filled, config));
        this.markDef = initMarkdef(markDef, encoding, config);
        this.size = initLayoutSize({
            encoding,
            size: isFrameMixins(spec)
                ? {
                    ...parentGivenSize,
                    ...(spec.width !== undefined ? { width: spec.width } : {}),
                    ...(spec.height !== undefined ? { height: spec.height } : {}),
                }
                : parentGivenSize,
        });
        // calculate stack properties
        this.stack = stack(this.markDef, encoding);
        this.specifiedScales = this.initScales(mark, encoding);
        this.specifiedAxes = this.initAxes(encoding);
        this.specifiedLegends = this.initLegends(encoding);
        this.specifiedProjection = spec.projection;
        // Selections will be initialized upon parse.
        this.selection = (spec.params ?? []).filter((p) => isSelectionParameter(p));
        this.alignStackOrderWithColorDomain();
    }
    get hasProjection() {
        const { encoding } = this;
        const isGeoShapeMark = this.mark === GEOSHAPE;
        const hasGeoPosition = encoding && GEOPOSITION_CHANNELS.some((channel) => isFieldOrDatumDef(encoding[channel]));
        return isGeoShapeMark || hasGeoPosition;
    }
    /**
     * Return specified Vega-Lite scale domain for a particular channel
     * @param channel
     */
    scaleDomain(channel) {
        const scale = this.specifiedScales[channel];
        return scale ? scale.domain : undefined;
    }
    axis(channel) {
        return this.specifiedAxes[channel];
    }
    legend(channel) {
        return this.specifiedLegends[channel];
    }
    initScales(mark, encoding) {
        return SCALE_CHANNELS.reduce((scales, channel) => {
            const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]);
            if (fieldOrDatumDef) {
                scales[channel] = this.initScale(fieldOrDatumDef.scale ?? {});
            }
            return scales;
        }, {});
    }
    initScale(scale) {
        const { domain, range } = scale;
        // TODO: we could simplify this function if we had a recursive replace function
        const scaleInternal = replaceExprRef(scale);
        if (isArray(domain)) {
            scaleInternal.domain = domain.map(signalRefOrValue);
        }
        if (isArray(range)) {
            scaleInternal.range = range.map(signalRefOrValue);
        }
        return scaleInternal;
    }
    initAxes(encoding) {
        return POSITION_SCALE_CHANNELS.reduce((_axis, channel) => {
            // Position Axis
            // TODO: handle ConditionFieldDef
            const channelDef = encoding[channel];
            if (isFieldOrDatumDef(channelDef) ||
                (channel === X && isFieldOrDatumDef(encoding.x2)) ||
                (channel === Y && isFieldOrDatumDef(encoding.y2))) {
                const axisSpec = isFieldOrDatumDef(channelDef) ? channelDef.axis : undefined;
                _axis[channel] = axisSpec
                    ? this.initAxis({ ...axisSpec }) // convert truthy value to object
                    : axisSpec;
            }
            return _axis;
        }, {});
    }
    initAxis(axis) {
        const props = keys(axis);
        const axisInternal = {};
        for (const prop of props) {
            const val = axis[prop];
            axisInternal[prop] = isConditionalAxisValue(val)
                ? signalOrValueRefWithCondition(val)
                : signalRefOrValue(val);
        }
        return axisInternal;
    }
    initLegends(encoding) {
        return NONPOSITION_SCALE_CHANNELS.reduce((_legend, channel) => {
            const fieldOrDatumDef = getFieldOrDatumDef(encoding[channel]);
            if (fieldOrDatumDef && supportLegend(channel)) {
                const legend = fieldOrDatumDef.legend;
                _legend[channel] = legend
                    ? replaceExprRef(legend) // convert truthy value to object
                    : legend;
            }
            return _legend;
        }, {});
    }
    /**
     * If this unit lacks order encoding but does contain a color domain
     * add transform and encoding that aligns the stack order with the color domain.
     */
    alignStackOrderWithColorDomain() {
        const { color, fill, order, xOffset, yOffset } = this.encoding;
        const colorField = fill || color;
        const colorEncoding = isFieldDef(colorField) ? colorField : undefined;
        const field = colorEncoding?.field;
        const scale = colorEncoding?.scale;
        const colorEncodingType = colorEncoding?.type;
        const domain = scale?.domain;
        const offset = xOffset || yOffset;
        const offsetEncoding = isFieldDef(offset) ? offset : undefined;
        const orderFieldName = `_${field}_sort_index`;
        if (!order && Array.isArray(domain) && typeof field === 'string' && colorEncodingType === 'nominal') {
            // align grouped chart order with color domain
            if (offsetEncoding && !offsetEncoding.sort) {
                offsetEncoding.sort = domain;
            }
            else {
                // align stacked chart order with color domain
                if (!this.stack) {
                    return;
                }
                const orderExpression = `indexof(${$(domain)}, datum['${field}'])`;
                const sort = this.markDef?.orient === 'horizontal' ? 'ascending' : 'descending';
                this.transforms.push({ calculate: orderExpression, as: orderFieldName });
                this.encoding.order = { field: orderFieldName, type: 'quantitative', sort };
            }
        }
    }
    parseData() {
        this.component.data = parseData(this);
    }
    parseLayoutSize() {
        parseUnitLayoutSize(this);
    }
    parseSelections() {
        this.component.selection = parseUnitSelection(this, this.selection);
    }
    parseMarkGroup() {
        this.component.mark = parseMarkGroups(this);
    }
    parseAxesAndHeaders() {
        this.component.axes = parseUnitAxes(this);
    }
    assembleSelectionTopLevelSignals(signals) {
        return assembleTopLevelSignals(this, signals);
    }
    assembleSignals() {
        return [...assembleAxisSignals(this), ...assembleUnitSelectionSignals(this, [])];
    }
    assembleSelectionData(data) {
        return assembleUnitSelectionData(this, data);
    }
    assembleLayout() {
        return null;
    }
    assembleLayoutSignals() {
        return assembleLayoutSignals(this);
    }
    /**
     * Corrects the data references in marks after assemble.
     */
    correctDataNames = (mark) => {
        // for normal data references
        if (mark.from?.data) {
            mark.from.data = this.lookupDataSource(mark.from.data);
            if ('time' in this.encoding) {
                mark.from.data = mark.from.data + CURR;
            }
        }
        // for access to facet data
        if (mark.from?.facet?.data) {
            mark.from.facet.data = this.lookupDataSource(mark.from.facet.data);
            // TOOD(jzong) uncomment this when it's time to implement facet animation
            // if ('time' in this.encoding) {
            //   mark.from.facet.data = mark.from.facet.data + CURR;
            // }
        }
        return mark;
    };
    assembleMarks() {
        let marks = this.component.mark ?? [];
        // If this unit is part of a layer, selections should augment
        // all in concert rather than each unit individually. This
        // ensures correct interleaving of clipping and brushed marks.
        if (!this.parent || !isLayerModel(this.parent)) {
            marks = assembleUnitSelectionMarks(this, marks);
        }
        return marks.map(this.correctDataNames);
    }
    assembleGroupStyle() {
        const { style } = this.view || {};
        if (style !== undefined) {
            return style;
        }
        if (this.encoding.x || this.encoding.y) {
            return 'cell';
        }
        else {
            return 'view';
        }
    }
    getMapping() {
        return this.encoding;
    }
    get mark() {
        return this.markDef.type;
    }
    channelHasField(channel) {
        return channelHasField(this.encoding, channel);
    }
    fieldDef(channel) {
        const channelDef = this.encoding[channel];
        return getFieldDef(channelDef);
    }
    typedFieldDef(channel) {
        const fieldDef = this.fieldDef(channel);
        if (isTypedFieldDef(fieldDef)) {
            return fieldDef;
        }
        return null;
    }
}

class LayerModel extends Model {
    // HACK: This should be (LayerModel | UnitModel)[], but setting the correct type leads to weird error.
    // So I'm just putting generic Model for now
    children;
    constructor(spec, parent, parentGivenName, parentGivenSize, config) {
        super(spec, 'layer', parent, parentGivenName, config, spec.resolve, spec.view);
        const layoutSize = {
            ...parentGivenSize,
            ...(spec.width ? { width: spec.width } : {}),
            ...(spec.height ? { height: spec.height } : {}),
        };
        this.children = spec.layer.map((layer, i) => {
            if (isLayerSpec(layer)) {
                return new LayerModel(layer, this, this.getName(`layer_${i}`), layoutSize, config);
            }
            else if (isUnitSpec(layer)) {
                return new UnitModel(layer, this, this.getName(`layer_${i}`), layoutSize, config);
            }
            throw new Error(invalidSpec(layer));
        });
    }
    parseData() {
        this.component.data = parseData(this);
        for (const child of this.children) {
            child.parseData();
        }
    }
    parseLayoutSize() {
        parseLayerLayoutSize(this);
    }
    parseSelections() {
        // Merge selections up the hierarchy so that they may be referenced
        // across unit specs. Persist their definitions within each child
        // to assemble signals which remain within output Vega unit groups.
        this.component.selection = {};
        for (const child of this.children) {
            child.parseSelections();
            for (const key of keys(child.component.selection)) {
                this.component.selection[key] = child.component.selection[key];
            }
        }
        if (vals(this.component.selection).some((selCmpt) => isTimerSelection(selCmpt))) {
            error(MULTI_VIEW_ANIMATION_UNSUPPORTED);
        }
    }
    parseMarkGroup() {
        for (const child of this.children) {
            child.parseMarkGroup();
        }
    }
    parseAxesAndHeaders() {
        parseLayerAxes(this);
    }
    assembleSelectionTopLevelSignals(signals) {
        return this.children.reduce((sg, child) => child.assembleSelectionTopLevelSignals(sg), signals);
    }
    // TODO: Support same named selections across children.
    assembleSignals() {
        return this.children.reduce((signals, child) => {
            return signals.concat(child.assembleSignals());
        }, assembleAxisSignals(this));
    }
    assembleLayoutSignals() {
        return this.children.reduce((signals, child) => {
            return signals.concat(child.assembleLayoutSignals());
        }, assembleLayoutSignals(this));
    }
    assembleSelectionData(data) {
        return this.children.reduce((db, child) => child.assembleSelectionData(db), data);
    }
    assembleGroupStyle() {
        const uniqueStyles = new Set();
        for (const child of this.children) {
            for (const style of array$2(child.assembleGroupStyle())) {
                uniqueStyles.add(style);
            }
        }
        const styles = Array.from(uniqueStyles);
        return styles.length > 1 ? styles : styles.length === 1 ? styles[0] : undefined;
    }
    assembleTitle() {
        let title = super.assembleTitle();
        if (title) {
            return title;
        }
        // If title does not provide layer, look into children
        for (const child of this.children) {
            title = child.assembleTitle();
            if (title) {
                return title;
            }
        }
        return undefined;
    }
    assembleLayout() {
        return null;
    }
    assembleMarks() {
        return assembleLayerSelectionMarks(this, this.children.flatMap((child) => {
            return child.assembleMarks();
        }));
    }
    assembleLegends() {
        return this.children.reduce((legends, child) => {
            return legends.concat(child.assembleLegends());
        }, assembleLegends(this));
    }
}

function buildModel(spec, parent, parentGivenName, unitSize, config) {
    if (isFacetSpec(spec)) {
        return new FacetModel(spec, parent, parentGivenName, config);
    }
    else if (isLayerSpec(spec)) {
        return new LayerModel(spec, parent, parentGivenName, unitSize, config);
    }
    else if (isUnitSpec(spec)) {
        return new UnitModel(spec, parent, parentGivenName, unitSize, config);
    }
    else if (isAnyConcatSpec(spec)) {
        return new ConcatModel(spec, parent, parentGivenName, config);
    }
    throw new Error(invalidSpec(spec));
}

/**
 * Vega-Lite's main function, for compiling Vega-Lite spec into Vega spec.
 *
 * At a high-level, we make the following transformations in different phases:
 *
 * Input spec
 *     |
 *     |  (Normalization)
 *     v
 * Normalized Spec (Row/Column channels in single-view specs becomes faceted specs, composite marks becomes layered specs.)
 *     |
 *     |  (Build Model)
 *     v
 * A model tree of the spec
 *     |
 *     |  (Parse)
 *     v
 * A model tree with parsed components (intermediate structure of visualization primitives in a format that can be easily merged)
 *     |
 *     | (Optimize)
 *     v
 * A model tree with parsed components with the data component optimized
 *     |
 *     | (Assemble)
 *     v
 * Vega spec
 *
 * @param inputSpec The Vega-Lite specification.
 * @param opt       Optional arguments passed to the Vega-Lite compiler.
 * @returns         An object containing the compiled Vega spec and normalized Vega-Lite spec.
 */
function compile(inputSpec, opt = {}) {
    // 0. Augment opt with default opts
    if (opt.logger) {
        // set the singleton logger to the provided logger
        set(opt.logger);
    }
    if (opt.fieldTitle) {
        // set the singleton field title formatter
        setTitleFormatter(opt.fieldTitle);
    }
    try {
        // 1. Initialize config by deep merging default config with the config provided via option and the input spec.
        const config = initConfig(mergeConfig(opt.config, inputSpec.config));
        // 2. Normalize: Convert input spec -> normalized spec
        // - Decompose all extended unit specs into composition of unit spec. For example, a box plot get expanded into multiple layers of bars, ticks, and rules. The shorthand row/column channel is also expanded to a facet spec.
        // - Normalize autosize and width or height spec
        const spec = normalize(inputSpec, config);
        // 3. Build Model: normalized spec -> Model (a tree structure)
        // This phases instantiates the models with default config by doing a top-down traversal. This allows us to pass properties that child models derive from their parents via their constructors.
        // See the abstract `Model` class and its children (UnitModel, LayerModel, FacetModel, ConcatModel) for different types of models.
        const model = buildModel(spec, null, '', undefined, config);
        // 4 Parse: Model --> Model with components
        // Note that components = intermediate representations that are equivalent to Vega specs.
        // We need these intermediate representation because we need to merge many visualization "components" like projections, scales, axes, and legends.
        // We will later convert these components into actual Vega specs in the assemble phase.
        // In this phase, we do a bottom-up traversal over the whole tree to
        // parse for each type of components once (e.g., data, layout, mark, scale).
        // By doing bottom-up traversal, we start parsing components of unit specs and
        // then merge child components of parent composite specs.
        //
        // Please see inside model.parse() for order of different components parsed.
        model.parse();
        // drawDataflow(model.component.data.sources);
        // 5. Optimize the dataflow. This will modify the data component of the model.
        optimizeDataflow(model.component.data, model);
        // drawDataflow(model.component.data.sources);
        // 6. Assemble: convert model components --> Vega Spec.
        const vgSpec = assembleTopLevelModel(model, getTopLevelProperties(inputSpec, spec.autosize, config, model), inputSpec.datasets, inputSpec.usermeta);
        return {
            spec: vgSpec,
            normalized: spec,
        };
    }
    finally {
        // Reset the singleton logger if a logger is provided
        if (opt.logger) {
            reset();
        }
        // Reset the singleton field title formatter if provided
        if (opt.fieldTitle) {
            resetTitleFormatter();
        }
    }
}
function getTopLevelProperties(inputSpec, autosize, config, model) {
    const width = model.component.layoutSize.get('width');
    const height = model.component.layoutSize.get('height');
    if (autosize === undefined) {
        autosize = { type: 'pad' };
        if (model.hasAxisOrientSignalRef()) {
            autosize.resize = true;
        }
    }
    else if (isString(autosize)) {
        autosize = { type: autosize };
    }
    if (width && height && isFitType(autosize.type)) {
        if (width === 'step' && height === 'step') {
            warn(droppingFit());
            autosize.type = 'pad';
        }
        else if (width === 'step' || height === 'step') {
            // effectively XOR, because else if
            // get step dimension
            const sizeType = width === 'step' ? 'width' : 'height';
            // log that we're dropping fit for respective channel
            warn(droppingFit(getPositionScaleChannel(sizeType)));
            // setting type to inverse fit (so if we dropped fit-x, type is now fit-y)
            const inverseSizeType = sizeType === 'width' ? 'height' : 'width';
            autosize.type = getFitType(inverseSizeType);
        }
    }
    return {
        ...(keys(autosize).length === 1 && autosize.type
            ? autosize.type === 'pad'
                ? {}
                : { autosize: autosize.type }
            : { autosize }),
        ...extractTopLevelProperties(config, false),
        ...extractTopLevelProperties(inputSpec, true),
    };
}
/*
 * Assemble the top-level model to a Vega spec.
 *
 * Note: this couldn't be `model.assemble()` since the top-level model
 * needs some special treatment to generate top-level properties.
 */
function assembleTopLevelModel(model, topLevelProperties, datasets = {}, usermeta) {
    // Config with Vega-Lite only config removed.
    const vgConfig = model.config ? stripAndRedirectConfig(model.config) : undefined;
    const rootData = assembleRootData(model.component.data, datasets);
    const data = model.assembleSelectionData(rootData);
    const projections = model.assembleProjections();
    const title = model.assembleTitle();
    const style = model.assembleGroupStyle();
    const encodeEntry = model.assembleGroupEncodeEntry(true);
    let layoutSignals = model.assembleLayoutSignals();
    // move width and height signals with values to top level
    layoutSignals = layoutSignals.filter((signal) => {
        if ((signal.name === 'width' || signal.name === 'height') && signal.value !== undefined) {
            topLevelProperties[signal.name] = +signal.value;
            return false;
        }
        return true;
    });
    const { params, ...otherTopLevelProps } = topLevelProperties;
    return {
        $schema: 'https://vega.github.io/schema/vega/v6.json',
        ...(model.description ? { description: model.description } : {}),
        ...otherTopLevelProps,
        ...(title ? { title } : {}),
        ...(style ? { style } : {}),
        ...(encodeEntry ? { encode: { update: encodeEntry } } : {}),
        data,
        ...(projections.length > 0 ? { projections } : {}),
        ...model.assembleGroup([
            ...layoutSignals,
            ...model.assembleSelectionTopLevelSignals([]),
            ...assembleParameterSignals(params),
        ]),
        ...(vgConfig ? { config: vgConfig } : {}),
        ...(usermeta ? { usermeta } : {}),
    };
}

const version$2 = pkg$1.version;

const vegaLiteImport = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  accessPathDepth,
  accessPathWithDatum,
  accessWithDatumToUnescapedPath,
  compile,
  contains,
  deepEqual,
  deleteNestedProperty,
  duplicate,
  entries: entries$1,
  every,
  fieldIntersection,
  flatAccessWithDatum,
  getFirstDefined,
  hasIntersection,
  hasProperty,
  hash,
  internalField,
  isBoolean,
  isEmpty,
  isEqual,
  isInternalField,
  isNullOrFalse,
  isNumeric,
  isPrimitive,
  keys,
  logicalExpr,
  mergeDeep: mergeDeep$1,
  never,
  normalize,
  normalizeAngle,
  omit,
  pick,
  prefixGenerator,
  removePathFromField,
  replaceAll,
  replacePathInField,
  resetIdCounter,
  setEqual,
  some,
  stringify: stringify$1,
  titleCase,
  unescapeSingleQuoteAndPathDot,
  unique,
  uniqueId,
  vals,
  varName,
  version: version$2
}, Symbol.toStringTag, { value: 'Module' }));

function e(e){const[n,r]=/schema\/([\w-]+)\/([\w\.\-]+)\.json$/g.exec(e).slice(1,3);return {library:n,version:r}}

var version$1 = "3.0.0";
var pkg = {
	version: version$1};

const lightColor = '#fff';
const medColor = '#888';
const darkTheme = {
    background: '#333',
    view: {
        stroke: medColor,
    },
    title: {
        color: lightColor,
        subtitleColor: lightColor,
    },
    style: {
        'guide-label': {
            fill: lightColor,
        },
        'guide-title': {
            fill: lightColor,
        },
    },
    axis: {
        domainColor: lightColor,
        gridColor: medColor,
        tickColor: lightColor,
    },
};

const markColor$7 = '#4572a7';
const excelTheme = {
    background: '#fff',
    arc: { fill: markColor$7 },
    area: { fill: markColor$7 },
    line: { stroke: markColor$7, strokeWidth: 2 },
    path: { stroke: markColor$7 },
    rect: { fill: markColor$7 },
    shape: { stroke: markColor$7 },
    symbol: { fill: markColor$7, strokeWidth: 1.5, size: 50 },
    axis: {
        bandPosition: 0.5,
        grid: true,
        gridColor: '#000000',
        gridOpacity: 1,
        gridWidth: 0.5,
        labelPadding: 10,
        tickSize: 5,
        tickWidth: 0.5,
    },
    axisBand: {
        grid: false,
        tickExtra: true,
    },
    legend: {
        labelBaseline: 'middle',
        labelFontSize: 11,
        symbolSize: 50,
        symbolType: 'square',
    },
    range: {
        category: [
            '#4572a7',
            '#aa4643',
            '#8aa453',
            '#71598e',
            '#4598ae',
            '#d98445',
            '#94aace',
            '#d09393',
            '#b9cc98',
            '#a99cbc',
        ],
    },
};

const markColor$6 = '#30a2da';
const axisColor$2 = '#cbcbcb';
const guideLabelColor = '#999';
const guideTitleColor = '#333';
const backgroundColor$2 = '#f0f0f0';
const blackTitle = '#333';
const fiveThirtyEightTheme = {
    arc: { fill: markColor$6 },
    area: { fill: markColor$6 },
    axis: {
        domainColor: axisColor$2,
        grid: true,
        gridColor: axisColor$2,
        gridWidth: 1,
        labelColor: guideLabelColor,
        labelFontSize: 10,
        titleColor: guideTitleColor,
        tickColor: axisColor$2,
        tickSize: 10,
        titleFontSize: 14,
        titlePadding: 10,
        labelPadding: 4,
    },
    axisBand: {
        grid: false,
    },
    background: backgroundColor$2,
    group: {
        fill: backgroundColor$2,
    },
    legend: {
        labelColor: blackTitle,
        labelFontSize: 11,
        padding: 1,
        symbolSize: 30,
        symbolType: 'square',
        titleColor: blackTitle,
        titleFontSize: 14,
        titlePadding: 10,
    },
    line: {
        stroke: markColor$6,
        strokeWidth: 2,
    },
    path: { stroke: markColor$6, strokeWidth: 0.5 },
    rect: { fill: markColor$6 },
    range: {
        category: [
            '#30a2da',
            '#fc4f30',
            '#e5ae38',
            '#6d904f',
            '#8b8b8b',
            '#b96db8',
            '#ff9e27',
            '#56cc60',
            '#52d2ca',
            '#52689e',
            '#545454',
            '#9fe4f8',
        ],
        diverging: ['#cc0020', '#e77866', '#f6e7e1', '#d6e8ed', '#91bfd9', '#1d78b5'],
        heatmap: ['#d6e8ed', '#cee0e5', '#91bfd9', '#549cc6', '#1d78b5'],
    },
    point: {
        filled: true,
        shape: 'circle',
    },
    shape: { stroke: markColor$6 },
    bar: {
        binSpacing: 2,
        fill: markColor$6,
        stroke: null,
    },
    title: {
        anchor: 'start',
        fontSize: 24,
        fontWeight: 600,
        offset: 20,
    },
};

const markColor$5 = '#000';
const ggplot2Theme = {
    group: {
        fill: '#e5e5e5',
    },
    arc: { fill: markColor$5 },
    area: { fill: markColor$5 },
    line: { stroke: markColor$5 },
    path: { stroke: markColor$5 },
    rect: { fill: markColor$5 },
    shape: { stroke: markColor$5 },
    symbol: { fill: markColor$5, size: 40 },
    axis: {
        domain: false,
        grid: true,
        gridColor: '#FFFFFF',
        gridOpacity: 1,
        labelColor: '#7F7F7F',
        labelPadding: 4,
        tickColor: '#7F7F7F',
        tickSize: 5.67,
        titleFontSize: 16,
        titleFontWeight: 'normal',
    },
    legend: {
        labelBaseline: 'middle',
        labelFontSize: 11,
        symbolSize: 40,
    },
    range: {
        category: [
            '#000000',
            '#7F7F7F',
            '#1A1A1A',
            '#999999',
            '#333333',
            '#B0B0B0',
            '#4D4D4D',
            '#C9C9C9',
            '#666666',
            '#DCDCDC',
        ],
    },
};

const headlineFontSize = 22;
const headlineFontWeight = 'normal';
const labelFont$1 = 'Benton Gothic, sans-serif';
const labelFontSize = 11.5;
const labelFontWeight = 'normal';
const markColor$4 = '#82c6df';
// const markHighlight = '#006d8f';
// const markDemocrat = '#5789b8';
// const markRepublican = '#d94f54';
const titleFont = 'Benton Gothic Bold, sans-serif';
const titleFontWeight = 'normal';
const titleFontSize$1 = 13;
const colorSchemes$1 = {
    'category-6': ['#ec8431', '#829eb1', '#c89d29', '#3580b1', '#adc839', '#ab7fb4'],
    'fire-7': ['#fbf2c7', '#f9e39c', '#f8d36e', '#f4bb6a', '#e68a4f', '#d15a40', '#ab4232'],
    'fireandice-6': ['#e68a4f', '#f4bb6a', '#f9e39c', '#dadfe2', '#a6b7c6', '#849eae']};
const latimesTheme = {
    background: '#ffffff',
    title: {
        anchor: 'start',
        color: '#000000',
        font: titleFont,
        fontSize: headlineFontSize,
        fontWeight: headlineFontWeight,
    },
    arc: { fill: markColor$4 },
    area: { fill: markColor$4 },
    line: { stroke: markColor$4, strokeWidth: 2 },
    path: { stroke: markColor$4 },
    rect: { fill: markColor$4 },
    shape: { stroke: markColor$4 },
    symbol: { fill: markColor$4, size: 30 },
    axis: {
        labelFont: labelFont$1,
        labelFontSize,
        labelFontWeight,
        titleFont,
        titleFontSize: titleFontSize$1,
        titleFontWeight,
    },
    axisX: {
        labelAngle: 0,
        labelPadding: 4,
        tickSize: 3,
    },
    axisY: {
        labelBaseline: 'middle',
        maxExtent: 45,
        minExtent: 45,
        tickSize: 2,
        titleAlign: 'left',
        titleAngle: 0,
        titleX: -45,
        titleY: -11,
    },
    legend: {
        labelFont: labelFont$1,
        labelFontSize,
        symbolType: 'square',
        titleFont,
        titleFontSize: titleFontSize$1,
        titleFontWeight,
    },
    range: {
        category: colorSchemes$1['category-6'],
        diverging: colorSchemes$1['fireandice-6'],
        heatmap: colorSchemes$1['fire-7'],
        ordinal: colorSchemes$1['fire-7'],
        ramp: colorSchemes$1['fire-7'],
    },
};

const markColor$3 = '#ab5787';
const axisColor$1 = '#979797';
const quartzTheme = {
    background: '#f9f9f9',
    arc: { fill: markColor$3 },
    area: { fill: markColor$3 },
    line: { stroke: markColor$3 },
    path: { stroke: markColor$3 },
    rect: { fill: markColor$3 },
    shape: { stroke: markColor$3 },
    symbol: { fill: markColor$3, size: 30 },
    axis: {
        domainColor: axisColor$1,
        domainWidth: 0.5,
        gridWidth: 0.2,
        labelColor: axisColor$1,
        tickColor: axisColor$1,
        tickWidth: 0.2,
        titleColor: axisColor$1,
    },
    axisBand: {
        grid: false,
    },
    axisX: {
        grid: true,
        tickSize: 10,
    },
    axisY: {
        domain: false,
        grid: true,
        tickSize: 0,
    },
    legend: {
        labelFontSize: 11,
        padding: 1,
        symbolSize: 30,
        symbolType: 'square',
    },
    range: {
        category: [
            '#ab5787',
            '#51b2e5',
            '#703c5c',
            '#168dd9',
            '#d190b6',
            '#00609f',
            '#d365ba',
            '#154866',
            '#666666',
            '#c4c4c4',
        ],
    },
};

const markColor$2 = '#3e5c69';
const voxTheme = {
    background: '#fff',
    arc: { fill: markColor$2 },
    area: { fill: markColor$2 },
    line: { stroke: markColor$2 },
    path: { stroke: markColor$2 },
    rect: { fill: markColor$2 },
    shape: { stroke: markColor$2 },
    symbol: { fill: markColor$2 },
    axis: {
        domainWidth: 0.5,
        grid: true,
        labelPadding: 2,
        tickSize: 5,
        tickWidth: 0.5,
        titleFontWeight: 'normal',
    },
    axisBand: {
        grid: false,
    },
    axisX: {
        gridWidth: 0.2,
    },
    axisY: {
        gridDash: [3],
        gridWidth: 0.4,
    },
    legend: {
        labelFontSize: 11,
        padding: 1,
        symbolType: 'square',
    },
    range: {
        category: ['#3e5c69', '#6793a6', '#182429', '#0570b0', '#3690c0', '#74a9cf', '#a6bddb', '#e2ddf2'],
    },
};

const markColor$1 = '#1696d2';
const axisColor = '#000000';
const backgroundColor$1 = '#FFFFFF';
const font = 'Lato';
const labelFont = 'Lato';
const sourceFont = 'Lato';
const gridColor$1 = '#DEDDDD';
const titleFontSize = 18;
const colorSchemes = {
    'shades-blue': ['#CFE8F3', '#A2D4EC', '#73BFE2', '#46ABDB', '#1696D2', '#12719E', '#0A4C6A', '#062635'],
    'six-groups-cat-1': ['#1696d2', '#ec008b', '#fdbf11', '#000000', '#d2d2d2', '#55b748'],
    'six-groups-seq': ['#cfe8f3', '#a2d4ec', '#73bfe2', '#46abdb', '#1696d2', '#12719e'],
    'diverging-colors': ['#ca5800', '#fdbf11', '#fdd870', '#fff2cf', '#cfe8f3', '#73bfe2', '#1696d2', '#0a4c6a'],
};
const urbanInstituteTheme = {
    background: backgroundColor$1,
    title: {
        anchor: 'start',
        fontSize: titleFontSize,
        font: font,
    },
    axisX: {
        domain: true,
        domainColor: axisColor,
        domainWidth: 1,
        grid: false,
        labelFontSize: 12,
        labelFont: labelFont,
        labelAngle: 0,
        tickColor: axisColor,
        tickSize: 5,
        titleFontSize: 12,
        titlePadding: 10,
        titleFont: font,
    },
    axisY: {
        domain: false,
        domainWidth: 1,
        grid: true,
        gridColor: gridColor$1,
        gridWidth: 1,
        labelFontSize: 12,
        labelFont: labelFont,
        labelPadding: 8,
        ticks: false,
        titleFontSize: 12,
        titlePadding: 10,
        titleFont: font,
        titleAngle: 0,
        titleY: -10,
        titleX: 18,
    },
    legend: {
        labelFontSize: 12,
        labelFont: labelFont,
        symbolSize: 100,
        titleFontSize: 12,
        titlePadding: 10,
        titleFont: font,
        orient: 'right',
        offset: 10,
    },
    view: {
        stroke: 'transparent',
    },
    range: {
        category: colorSchemes['six-groups-cat-1'],
        diverging: colorSchemes['diverging-colors'],
        heatmap: colorSchemes['diverging-colors'],
        ordinal: colorSchemes['six-groups-seq'],
        ramp: colorSchemes['shades-blue'],
    },
    area: {
        fill: markColor$1,
    },
    rect: {
        fill: markColor$1,
    },
    line: {
        color: markColor$1,
        stroke: markColor$1,
        strokeWidth: 5,
    },
    trail: {
        color: markColor$1,
        stroke: markColor$1,
        strokeWidth: 0,
        size: 1,
    },
    path: {
        stroke: markColor$1,
        strokeWidth: 0.5,
    },
    point: {
        filled: true,
    },
    text: {
        font: sourceFont,
        color: markColor$1,
        fontSize: 11,
        align: 'center',
        fontWeight: 400,
        size: 11,
    },
    style: {
        bar: {
            fill: markColor$1,
            stroke: null,
        },
    },
    arc: { fill: markColor$1 },
    shape: { stroke: markColor$1 },
    symbol: { fill: markColor$1, size: 30 },
};

/**
 * Copyright 2020 Google LLC.
 *
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file or at
 * https://developers.google.com/open-source/licenses/bsd
 */
const markColor = '#3366CC';
const gridColor = '#ccc';
const defaultFont$1 = 'Arial, sans-serif';
const googlechartsTheme = {
    arc: { fill: markColor },
    area: { fill: markColor },
    path: { stroke: markColor },
    rect: { fill: markColor },
    shape: { stroke: markColor },
    symbol: { stroke: markColor },
    circle: { fill: markColor },
    background: '#fff',
    padding: {
        top: 10,
        right: 10,
        bottom: 10,
        left: 10,
    },
    style: {
        'guide-label': {
            font: defaultFont$1,
            fontSize: 12,
        },
        'guide-title': {
            font: defaultFont$1,
            fontSize: 12,
        },
        'group-title': {
            font: defaultFont$1,
            fontSize: 12,
        },
    },
    title: {
        font: defaultFont$1,
        fontSize: 14,
        fontWeight: 'bold',
        dy: -3,
        anchor: 'start',
    },
    axis: {
        gridColor: gridColor,
        tickColor: gridColor,
        domain: false,
        grid: true,
    },
    range: {
        category: [
            '#4285F4',
            '#DB4437',
            '#F4B400',
            '#0F9D58',
            '#AB47BC',
            '#00ACC1',
            '#FF7043',
            '#9E9D24',
            '#5C6BC0',
            '#F06292',
            '#00796B',
            '#C2185B',
        ],
        heatmap: ['#c6dafc', '#5e97f6', '#2a56c6'],
    },
};

const ptToPx = (value) => value * (1 / 3 + 1);
const fontSmallPx = ptToPx(9);
const legendFontPx = ptToPx(10);
const fontLargePx = ptToPx(12);
const fontStandard = 'Segoe UI';
const fontTitle = 'wf_standard-font, helvetica, arial, sans-serif';
const firstLevelElementColor = '#252423';
const secondLevelElementColor = '#605E5C';
const backgroundColor = 'transparent';
const backgroundSecondaryColor = '#C8C6C4';
const paletteColor1 = '#118DFF';
const paletteColor2 = '#12239E';
const paletteColor3 = '#E66C37';
const paletteColor4 = '#6B007B';
const paletteColor5 = '#E044A7';
const paletteColor6 = '#744EC2';
const paletteColor7 = '#D9B300';
const paletteColor8 = '#D64550';
const divergentColorMax = paletteColor1;
const divergentColorMin = '#DEEFFF';
const divergentPalette = [divergentColorMin, divergentColorMax];
const ordinalPalette = [
    divergentColorMin,
    '#c7e4ff',
    '#b0d9ff',
    '#9aceff',
    '#83c3ff',
    '#6cb9ff',
    '#55aeff',
    '#3fa3ff',
    '#2898ff',
    divergentColorMax,
];
const powerbiTheme = {
    view: { stroke: backgroundColor },
    background: backgroundColor,
    font: fontStandard,
    header: {
        titleFont: fontTitle,
        titleFontSize: fontLargePx,
        titleColor: firstLevelElementColor,
        labelFont: fontStandard,
        labelFontSize: legendFontPx,
        labelColor: secondLevelElementColor,
    },
    axis: {
        ticks: false,
        grid: false,
        domain: false,
        labelColor: secondLevelElementColor,
        labelFontSize: fontSmallPx,
        titleFont: fontTitle,
        titleColor: firstLevelElementColor,
        titleFontSize: fontLargePx,
        titleFontWeight: 'normal',
    },
    axisQuantitative: {
        tickCount: 3,
        grid: true,
        gridColor: backgroundSecondaryColor,
        gridDash: [1, 5],
        labelFlush: false,
    },
    axisBand: { tickExtra: true },
    axisX: { labelPadding: 5 },
    axisY: { labelPadding: 10 },
    bar: { fill: paletteColor1 },
    line: {
        stroke: paletteColor1,
        strokeWidth: 3,
        strokeCap: 'round',
        strokeJoin: 'round',
    },
    text: { font: fontStandard, fontSize: fontSmallPx, fill: secondLevelElementColor },
    arc: { fill: paletteColor1 },
    area: { fill: paletteColor1, line: true, opacity: 0.6 },
    path: { stroke: paletteColor1 },
    rect: { fill: paletteColor1 },
    point: { fill: paletteColor1, filled: true, size: 75 },
    shape: { stroke: paletteColor1 },
    symbol: { fill: paletteColor1, strokeWidth: 1.5, size: 50 },
    legend: {
        titleFont: fontStandard,
        titleFontWeight: 'bold',
        titleColor: secondLevelElementColor,
        labelFont: fontStandard,
        labelFontSize: legendFontPx,
        labelColor: secondLevelElementColor,
        symbolType: 'circle',
        symbolSize: 75,
    },
    range: {
        category: [
            paletteColor1,
            paletteColor2,
            paletteColor3,
            paletteColor4,
            paletteColor5,
            paletteColor6,
            paletteColor7,
            paletteColor8,
        ],
        diverging: divergentPalette,
        heatmap: divergentPalette,
        ordinal: ordinalPalette,
    },
};

const defaultFont = 'IBM Plex Sans,system-ui,-apple-system,BlinkMacSystemFont,".sfnstext-regular",sans-serif';
const condensedFont = 'IBM Plex Sans Condensed, system-ui, -apple-system, BlinkMacSystemFont, ".SFNSText-Regular", sans-serif';
const fontWeight = 400;
const TOKENS = {
    textPrimary: { g90: '#f4f4f4', g100: '#f4f4f4', white: '#161616', g10: '#161616' },
    textSecondary: { g90: '#c6c6c6', g100: '#c6c6c6', white: '#525252', g10: '#525252' },
    // layer
    layerAccent01: {
        white: '#e0e0e0',
        g10: '#e0e0e0',
        g90: '#525252',
        g100: '#393939',
    },
    // grid
    gridBg: {
        white: '#ffffff',
        g10: '#ffffff',
        g90: '#161616',
        g100: '#161616',
    },
};
const darkCategories = [
    '#8a3ffc',
    '#33b1ff',
    '#007d79',
    '#ff7eb6',
    '#fa4d56',
    '#fff1f1',
    '#6fdc8c',
    '#4589ff',
    '#d12771',
    '#d2a106',
    '#08bdba',
    '#bae6ff',
    '#ba4e00',
    '#d4bbff',
];
const lightCategories = [
    '#6929c4',
    '#1192e8',
    '#005d5d',
    '#9f1853',
    '#fa4d56',
    '#570408',
    '#198038',
    '#002d9c',
    '#ee538b',
    '#b28600',
    '#009d9a',
    '#012749',
    '#8a3800',
    '#a56eff',
];
function genCarbonConfig({ theme, background }) {
    const type = ['white', 'g10'].includes(theme) ? 'light' : 'dark';
    const viewbg = TOKENS.gridBg[theme];
    const titleColor = TOKENS.textPrimary[theme];
    const textColor = TOKENS.textSecondary[theme];
    const category = type === 'dark' ? darkCategories : lightCategories;
    const markColor = type === 'dark' ? '#d4bbff' : '#6929c4';
    return {
        background,
        arc: { fill: markColor },
        area: { fill: markColor },
        path: { stroke: markColor },
        rect: { fill: markColor },
        shape: { stroke: markColor },
        symbol: { stroke: markColor },
        circle: { fill: markColor },
        view: {
            fill: viewbg,
            stroke: viewbg,
        },
        group: {
            fill: viewbg,
        },
        title: {
            color: titleColor,
            anchor: 'start',
            dy: -15,
            fontSize: 16,
            font: defaultFont,
            fontWeight: 600,
        },
        axis: {
            // Axis labels
            labelColor: textColor,
            labelFontSize: 12,
            labelFont: condensedFont,
            labelFontWeight: fontWeight,
            // Axis titles
            titleColor: titleColor,
            titleFontWeight: 600,
            titleFontSize: 12,
            // MISC
            grid: true,
            gridColor: TOKENS.layerAccent01[theme],
            labelAngle: 0,
        },
        axisX: {
            titlePadding: 10,
        },
        axisY: {
            titlePadding: 2.5,
        },
        style: {
            'guide-label': {
                font: defaultFont,
                fill: textColor,
                fontWeight: fontWeight,
            },
            'guide-title': {
                font: defaultFont,
                fill: textColor,
                fontWeight: fontWeight,
            },
        },
        range: {
            category,
            diverging: [
                '#750e13',
                '#a2191f',
                '#da1e28',
                '#fa4d56',
                '#ff8389',
                '#ffb3b8',
                '#ffd7d9',
                '#fff1f1',
                '#e5f6ff',
                '#bae6ff',
                '#82cfff',
                '#33b1ff',
                '#1192e8',
                '#0072c3',
                '#00539a',
                '#003a6d',
            ],
            heatmap: [
                '#f6f2ff',
                '#e8daff',
                '#d4bbff',
                '#be95ff',
                '#a56eff',
                '#8a3ffc',
                '#6929c4',
                '#491d8b',
                '#31135e',
                '#1c0f30',
            ],
        },
    };
}

const carbonwhite = genCarbonConfig({
    theme: 'white',
    background: '#ffffff',
});

const carbong10 = genCarbonConfig({
    theme: 'g10',
    background: '#f4f4f4',
});

const carbong90 = genCarbonConfig({
    theme: 'g90',
    background: '#262626',
});

const carbong100 = genCarbonConfig({
    theme: 'g100',
    background: '#161616',
});

const version = pkg.version;

const themes = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
  __proto__: null,
  carbong10,
  carbong100,
  carbong90,
  carbonwhite,
  dark: darkTheme,
  excel: excelTheme,
  fivethirtyeight: fiveThirtyEightTheme,
  ggplot2: ggplot2Theme,
  googlecharts: googlechartsTheme,
  latimes: latimesTheme,
  powerbi: powerbiTheme,
  quartz: quartzTheme,
  urbaninstitute: urbanInstituteTheme,
  version,
  vox: voxTheme
}, Symbol.toStringTag, { value: 'Module' }));

/**
 * Format the value to be shown in the tooltip.
 *
 * @param value The value to show in the tooltip.
 * @param valueToHtml Function to convert a single cell value to an HTML string
 */
function formatValue(value, valueToHtml, maxDepth, baseURL) {
    if (isArray(value)) {
        return `[${value.map((v) => valueToHtml(isString(v) ? v : stringify(v, maxDepth))).join(', ')}]`;
    }
    if (isObject(value)) {
        let content = '';
        const { title, image, ...rest } = value;
        if (title) {
            content += `<h2>${valueToHtml(title)}</h2>`;
        }
        if (image) {
            content += `<img src="${new URL(valueToHtml(image), baseURL || location.href).href}">`;
        }
        const keys = Object.keys(rest);
        if (keys.length > 0) {
            content += '<table>';
            for (const key of keys) {
                let val = rest[key];
                // ignore undefined properties
                if (val === undefined) {
                    continue;
                }
                if (isObject(val)) {
                    val = stringify(val, maxDepth);
                }
                content += `<tr><td class="key">${valueToHtml(key)}</td><td class="value">${valueToHtml(val)}</td></tr>`;
            }
            content += `</table>`;
        }
        return content || '{}'; // show empty object if there are no properties
    }
    return valueToHtml(value);
}
function replacer(maxDepth) {
    const stack = [];
    return function (key, value) {
        if (typeof value !== 'object' || value === null) {
            return value;
        }
        const pos = stack.indexOf(this) + 1;
        stack.length = pos;
        if (stack.length > maxDepth) {
            return '[Object]';
        }
        if (stack.indexOf(value) >= 0) {
            return '[Circular]';
        }
        stack.push(value);
        return value;
    };
}
/**
 * Stringify any JS object to valid JSON
 */
function stringify(obj, maxDepth) {
    return JSON.stringify(obj, replacer(maxDepth));
}

// generated with build-style.sh
var defaultStyle = `#vg-tooltip-element {
  visibility: hidden;
  padding: 8px;
  position: fixed;
  z-index: 1000;
  font-family: sans-serif;
  font-size: 11px;
  border-radius: 3px;
  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
  /* The default theme is the light theme. */
  background-color: rgba(255, 255, 255, 0.95);
  border: 1px solid #d9d9d9;
  color: black;
}
#vg-tooltip-element.visible {
  visibility: visible;
}
#vg-tooltip-element h2 {
  margin-top: 0;
  margin-bottom: 10px;
  font-size: 13px;
}
#vg-tooltip-element table {
  border-spacing: 0;
}
#vg-tooltip-element table tr {
  border: none;
}
#vg-tooltip-element table tr td {
  overflow: hidden;
  text-overflow: ellipsis;
  padding-top: 2px;
  padding-bottom: 2px;
}
#vg-tooltip-element table tr td.key {
  color: #808080;
  max-width: 150px;
  text-align: right;
  padding-right: 4px;
}
#vg-tooltip-element table tr td.value {
  display: block;
  max-width: 300px;
  max-height: 7em;
  text-align: left;
}
#vg-tooltip-element.dark-theme {
  background-color: rgba(32, 32, 32, 0.9);
  border: 1px solid #f5f5f5;
  color: white;
}
#vg-tooltip-element.dark-theme td.key {
  color: #bfbfbf;
}
`;

const EL_ID = 'vg-tooltip-element';
const DEFAULT_OPTIONS = {
    offsetX: 10,
    offsetY: 10,
    id: EL_ID,
    styleId: 'vega-tooltip-style',
    theme: 'light',
    disableDefaultStyle: false,
    sanitize: escapeHTML,
    maxDepth: 2,
    formatTooltip: formatValue,
    baseURL: '',
    anchor: 'cursor',
    position: ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
};
/**
 * Escape special HTML characters.
 *
 * @param value A value to convert to string and HTML-escape.
 */
function escapeHTML(value) {
    return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;');
}
function createDefaultStyle(id) {
    // Just in case this id comes from a user, ensure these is no security issues
    if (!/^[A-Za-z]+[-:.\w]*$/.test(id)) {
        throw new Error('Invalid HTML ID');
    }
    return defaultStyle.toString().replaceAll(EL_ID, id);
}

/**
 * Position the tooltip
 *
 * @param event The mouse event.
 * @param tooltipBox
 * @param options Tooltip handler options.
 */
function calculatePositionRelativeToCursor(event, tooltipBox, { offsetX, offsetY }) {
    // the possible positions for the tooltip
    const positions = getPositions({ x1: event.clientX, x2: event.clientX, y1: event.clientY, y2: event.clientY }, tooltipBox, offsetX, offsetY);
    // order of positions to try
    const postionArr = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
    // test positions till a valid one is found
    for (const p of postionArr) {
        if (tooltipIsInViewport(positions[p], tooltipBox)) {
            return positions[p];
        }
    }
    // default to top-left if a valid position is not found
    // this is legacy behavior
    return positions['top-left'];
}
/**
 * Calculates the position of the tooltip relative to the mark.
 * @param handler The handler instance.
 * @param event The mouse event.
 * @param item The item that the tooltip is being shown for.
 * @param tooltipBox Client rect of the tooltip element.
 * @param options Tooltip handler options.
 * @returns
 */
function calculatePositionRelativeToMark(handler, event, item, tooltipBox, options) {
    const { position, offsetX, offsetY } = options;
    const containerBox = handler._el.getBoundingClientRect();
    const origin = handler._origin;
    // bounds of the mark relative to the viewport
    const markBounds = getMarkBounds(containerBox, origin, item);
    // the possible positions for the tooltip
    const positions = getPositions(markBounds, tooltipBox, offsetX, offsetY);
    // positions to test
    const positionArr = Array.isArray(position) ? position : [position];
    // test positions till a valid one is found
    for (const p of positionArr) {
        // verify that the tooltip is in the view and the mouse is not where the tooltip would be
        if (tooltipIsInViewport(positions[p], tooltipBox) && !mouseIsOnTooltip(event, positions[p], tooltipBox)) {
            return positions[p];
        }
    }
    // default to cursor position if a valid position is not found
    return calculatePositionRelativeToCursor(event, tooltipBox, options);
}
// Calculates the bounds of the mark relative to the viewport.
function getMarkBounds(containerBox, origin, item) {
    // if this is a voronoi mark, we want to use the bounds of the point that voronoi cell represents
    const markBounds = item.isVoronoi ? item.datum.bounds : item.bounds;
    let left = containerBox.left + origin[0] + markBounds.x1;
    let top = containerBox.top + origin[1] + markBounds.y1;
    // traverse mark groups, summing their offsets to get the total offset
    // item bounds are relative to their group so if there are multiple nested groups we need to add them all
    let parentItem = item;
    while (parentItem.mark.group) {
        parentItem = parentItem.mark.group;
        left += parentItem.x ?? 0;
        top += parentItem.y ?? 0;
    }
    const markWidth = markBounds.x2 - markBounds.x1;
    const markHeight = markBounds.y2 - markBounds.y1;
    return {
        x1: left,
        x2: left + markWidth,
        y1: top,
        y2: top + markHeight,
    };
}
// Calculates the tooltip xy for each possible position.
function getPositions(markBounds, tooltipBox, offsetX, offsetY) {
    const xc = (markBounds.x1 + markBounds.x2) / 2;
    const yc = (markBounds.y1 + markBounds.y2) / 2;
    // x positions
    const left = markBounds.x1 - tooltipBox.width - offsetX;
    const center = xc - tooltipBox.width / 2;
    const right = markBounds.x2 + offsetX;
    // y positions
    const top = markBounds.y1 - tooltipBox.height - offsetY;
    const middle = yc - tooltipBox.height / 2;
    const bottom = markBounds.y2 + offsetY;
    const positions = {
        top: { x: center, y: top },
        bottom: { x: center, y: bottom },
        left: { x: left, y: middle },
        right: { x: right, y: middle },
        'top-left': { x: left, y: top },
        'top-right': { x: right, y: top },
        'bottom-left': { x: left, y: bottom },
        'bottom-right': { x: right, y: bottom },
    };
    return positions;
}
// Checks if the tooltip would be in the viewport at the given position
function tooltipIsInViewport(position, tooltipBox) {
    return (position.x >= 0 &&
        position.y >= 0 &&
        position.x + tooltipBox.width <= window.innerWidth &&
        position.y + tooltipBox.height <= window.innerHeight);
}
// Checks if the mouse is within the tooltip area
function mouseIsOnTooltip(event, position, tooltipBox) {
    return (event.clientX >= position.x &&
        event.clientX <= position.x + tooltipBox.width &&
        event.clientY >= position.y &&
        event.clientY <= position.y + tooltipBox.height);
}

/**
 * The tooltip handler class.
 */
class Handler {
    /**
     * The handler function. We bind this to this function in the constructor.
     */
    call;
    /**
     * Complete tooltip options.
     */
    options;
    /**
     * The tooltip html element.
     */
    el;
    /**
     * Create the tooltip handler and initialize the element and style.
     *
     * @param options Tooltip Options
     */
    constructor(options) {
        this.options = { ...DEFAULT_OPTIONS, ...options };
        const elementId = this.options.id;
        this.el = null;
        // bind this to call
        this.call = this.tooltipHandler.bind(this);
        // prepend a default stylesheet for tooltips to the head
        if (!this.options.disableDefaultStyle && !document.getElementById(this.options.styleId)) {
            const style = document.createElement('style');
            style.setAttribute('id', this.options.styleId);
            style.innerHTML = createDefaultStyle(elementId);
            const head = document.head;
            if (head.childNodes.length > 0) {
                head.insertBefore(style, head.childNodes[0]);
            }
            else {
                head.appendChild(style);
            }
        }
    }
    /**
     * The tooltip handler function.
     */
    tooltipHandler(handler, event, item, value) {
        // append a div element that we use as a tooltip unless it already exists
        this.el = document.getElementById(this.options.id);
        if (!this.el) {
            this.el = document.createElement('div');
            this.el.setAttribute('id', this.options.id);
            this.el.classList.add('vg-tooltip');
            const tooltipContainer = document.fullscreenElement ?? document.body;
            tooltipContainer.appendChild(this.el);
        }
        // hide tooltip for null, undefined, or empty string values
        if (value == null || value === '') {
            this.el.classList.remove('visible', `${this.options.theme}-theme`);
            return;
        }
        // set the tooltip content
        this.el.innerHTML = this.options.formatTooltip(value, this.options.sanitize, this.options.maxDepth, this.options.baseURL);
        // make the tooltip visible
        this.el.classList.add('visible', `${this.options.theme}-theme`);
        const { x, y } = this.options.anchor === 'mark'
            ? calculatePositionRelativeToMark(handler, event, item, this.el.getBoundingClientRect(), this.options)
            : calculatePositionRelativeToCursor(event, this.el.getBoundingClientRect(), this.options);
        this.el.style.top = `${y}px`;
        this.el.style.left = `${x}px`;
    }
}

var define_process_env_default = {};
/*!
 * https://github.com/Starcounter-Jack/JSON-Patch
 * (c) 2017-2022 Joachim Wester
 * MIT licensed
 */
var __extends = /* @__PURE__ */ (function() {
  var extendStatics = function(d, b) {
    extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) {
      d2.__proto__ = b2;
    } || function(d2, b2) {
      for (var p in b2) if (b2.hasOwnProperty(p)) d2[p] = b2[p];
    };
    return extendStatics(d, b);
  };
  return function(d, b) {
    extendStatics(d, b);
    function __() {
      this.constructor = d;
    }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  };
})();
var _hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwnProperty(obj, key) {
  return _hasOwnProperty.call(obj, key);
}
function _objectKeys(obj) {
  if (Array.isArray(obj)) {
    var keys_1 = new Array(obj.length);
    for (var k = 0; k < keys_1.length; k++) {
      keys_1[k] = "" + k;
    }
    return keys_1;
  }
  if (Object.keys) {
    return Object.keys(obj);
  }
  var keys = [];
  for (var i in obj) {
    if (hasOwnProperty(obj, i)) {
      keys.push(i);
    }
  }
  return keys;
}
function _deepClone(obj) {
  switch (typeof obj) {
    case "object":
      return JSON.parse(JSON.stringify(obj));
    //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
    case "undefined":
      return null;
    //this is how JSON.stringify behaves for array items
    default:
      return obj;
  }
}
function isInteger(str) {
  var i = 0;
  var len = str.length;
  var charCode;
  while (i < len) {
    charCode = str.charCodeAt(i);
    if (charCode >= 48 && charCode <= 57) {
      i++;
      continue;
    }
    return false;
  }
  return true;
}
function escapePathComponent(path) {
  if (path.indexOf("/") === -1 && path.indexOf("~") === -1)
    return path;
  return path.replace(/~/g, "~0").replace(/\//g, "~1");
}
function unescapePathComponent(path) {
  return path.replace(/~1/g, "/").replace(/~0/g, "~");
}
function hasUndefined(obj) {
  if (obj === void 0) {
    return true;
  }
  if (obj) {
    if (Array.isArray(obj)) {
      for (var i_1 = 0, len = obj.length; i_1 < len; i_1++) {
        if (hasUndefined(obj[i_1])) {
          return true;
        }
      }
    } else if (typeof obj === "object") {
      var objKeys = _objectKeys(obj);
      var objKeysLength = objKeys.length;
      for (var i = 0; i < objKeysLength; i++) {
        if (hasUndefined(obj[objKeys[i]])) {
          return true;
        }
      }
    }
  }
  return false;
}
function patchErrorMessageFormatter(message, args) {
  var messageParts = [message];
  for (var key in args) {
    var value = typeof args[key] === "object" ? JSON.stringify(args[key], null, 2) : args[key];
    if (typeof value !== "undefined") {
      messageParts.push(key + ": " + value);
    }
  }
  return messageParts.join("\n");
}
var PatchError = (
  /** @class */
  (function(_super) {
    __extends(PatchError2, _super);
    function PatchError2(message, name, index, operation, tree) {
      var _newTarget = this.constructor;
      var _this = _super.call(this, patchErrorMessageFormatter(message, { name, index, operation, tree })) || this;
      _this.name = name;
      _this.index = index;
      _this.operation = operation;
      _this.tree = tree;
      Object.setPrototypeOf(_this, _newTarget.prototype);
      _this.message = patchErrorMessageFormatter(message, { name, index, operation, tree });
      return _this;
    }
    return PatchError2;
  })(Error)
);
var JsonPatchError = PatchError;
var deepClone = _deepClone;
var objOps = {
  add: function(obj, key, document2) {
    obj[key] = this.value;
    return { newDocument: document2 };
  },
  remove: function(obj, key, document2) {
    var removed = obj[key];
    delete obj[key];
    return { newDocument: document2, removed };
  },
  replace: function(obj, key, document2) {
    var removed = obj[key];
    obj[key] = this.value;
    return { newDocument: document2, removed };
  },
  move: function(obj, key, document2) {
    var removed = getValueByPointer(document2, this.path);
    if (removed) {
      removed = _deepClone(removed);
    }
    var originalValue = applyOperation(document2, { op: "remove", path: this.from }).removed;
    applyOperation(document2, { op: "add", path: this.path, value: originalValue });
    return { newDocument: document2, removed };
  },
  copy: function(obj, key, document2) {
    var valueToCopy = getValueByPointer(document2, this.from);
    applyOperation(document2, { op: "add", path: this.path, value: _deepClone(valueToCopy) });
    return { newDocument: document2 };
  },
  test: function(obj, key, document2) {
    return { newDocument: document2, test: _areEquals(obj[key], this.value) };
  },
  _get: function(obj, key, document2) {
    this.value = obj[key];
    return { newDocument: document2 };
  }
};
var arrOps = {
  add: function(arr, i, document2) {
    if (isInteger(i)) {
      arr.splice(i, 0, this.value);
    } else {
      arr[i] = this.value;
    }
    return { newDocument: document2, index: i };
  },
  remove: function(arr, i, document2) {
    var removedList = arr.splice(i, 1);
    return { newDocument: document2, removed: removedList[0] };
  },
  replace: function(arr, i, document2) {
    var removed = arr[i];
    arr[i] = this.value;
    return { newDocument: document2, removed };
  },
  move: objOps.move,
  copy: objOps.copy,
  test: objOps.test,
  _get: objOps._get
};
function getValueByPointer(document2, pointer) {
  if (pointer == "") {
    return document2;
  }
  var getOriginalDestination = { op: "_get", path: pointer };
  applyOperation(document2, getOriginalDestination);
  return getOriginalDestination.value;
}
function applyOperation(document2, operation, validateOperation, mutateDocument, banPrototypeModifications, index) {
  if (validateOperation === void 0) {
    validateOperation = false;
  }
  if (mutateDocument === void 0) {
    mutateDocument = true;
  }
  if (banPrototypeModifications === void 0) {
    banPrototypeModifications = true;
  }
  if (index === void 0) {
    index = 0;
  }
  if (validateOperation) {
    if (typeof validateOperation == "function") {
      validateOperation(operation, 0, document2, operation.path);
    } else {
      validator(operation, 0);
    }
  }
  if (operation.path === "") {
    var returnValue = { newDocument: document2 };
    if (operation.op === "add") {
      returnValue.newDocument = operation.value;
      return returnValue;
    } else if (operation.op === "replace") {
      returnValue.newDocument = operation.value;
      returnValue.removed = document2;
      return returnValue;
    } else if (operation.op === "move" || operation.op === "copy") {
      returnValue.newDocument = getValueByPointer(document2, operation.from);
      if (operation.op === "move") {
        returnValue.removed = document2;
      }
      return returnValue;
    } else if (operation.op === "test") {
      returnValue.test = _areEquals(document2, operation.value);
      if (returnValue.test === false) {
        throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document2);
      }
      returnValue.newDocument = document2;
      return returnValue;
    } else if (operation.op === "remove") {
      returnValue.removed = document2;
      returnValue.newDocument = null;
      return returnValue;
    } else if (operation.op === "_get") {
      operation.value = document2;
      return returnValue;
    } else {
      if (validateOperation) {
        throw new JsonPatchError("Operation `op` property is not one of operations defined in RFC-6902", "OPERATION_OP_INVALID", index, operation, document2);
      } else {
        return returnValue;
      }
    }
  } else {
    if (!mutateDocument) {
      document2 = _deepClone(document2);
    }
    var path = operation.path || "";
    var keys = path.split("/");
    var obj = document2;
    var t = 1;
    var len = keys.length;
    var existingPathFragment = void 0;
    var key = void 0;
    var validateFunction = void 0;
    if (typeof validateOperation == "function") {
      validateFunction = validateOperation;
    } else {
      validateFunction = validator;
    }
    while (true) {
      key = keys[t];
      if (key && key.indexOf("~") != -1) {
        key = unescapePathComponent(key);
      }
      if (banPrototypeModifications && (key == "__proto__" || key == "prototype" && t > 0 && keys[t - 1] == "constructor")) {
        throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");
      }
      if (validateOperation) {
        if (existingPathFragment === void 0) {
          if (obj[key] === void 0) {
            existingPathFragment = keys.slice(0, t).join("/");
          } else if (t == len - 1) {
            existingPathFragment = operation.path;
          }
          if (existingPathFragment !== void 0) {
            validateFunction(operation, 0, document2, existingPathFragment);
          }
        }
      }
      t++;
      if (Array.isArray(obj)) {
        if (key === "-") {
          key = obj.length;
        } else {
          if (validateOperation && !isInteger(key)) {
            throw new JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", index, operation, document2);
          } else if (isInteger(key)) {
            key = ~~key;
          }
        }
        if (t >= len) {
          if (validateOperation && operation.op === "add" && key > obj.length) {
            throw new JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document2);
          }
          var returnValue = arrOps[operation.op].call(operation, obj, key, document2);
          if (returnValue.test === false) {
            throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document2);
          }
          return returnValue;
        }
      } else {
        if (t >= len) {
          var returnValue = objOps[operation.op].call(operation, obj, key, document2);
          if (returnValue.test === false) {
            throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document2);
          }
          return returnValue;
        }
      }
      obj = obj[key];
      if (validateOperation && t < len && (!obj || typeof obj !== "object")) {
        throw new JsonPatchError("Cannot perform operation at the desired path", "OPERATION_PATH_UNRESOLVABLE", index, operation, document2);
      }
    }
  }
}
function applyPatch(document2, patch, validateOperation, mutateDocument, banPrototypeModifications) {
  if (mutateDocument === void 0) {
    mutateDocument = true;
  }
  if (banPrototypeModifications === void 0) {
    banPrototypeModifications = true;
  }
  if (validateOperation) {
    if (!Array.isArray(patch)) {
      throw new JsonPatchError("Patch sequence must be an array", "SEQUENCE_NOT_AN_ARRAY");
    }
  }
  if (!mutateDocument) {
    document2 = _deepClone(document2);
  }
  var results = new Array(patch.length);
  for (var i = 0, length_1 = patch.length; i < length_1; i++) {
    results[i] = applyOperation(document2, patch[i], validateOperation, true, banPrototypeModifications, i);
    document2 = results[i].newDocument;
  }
  results.newDocument = document2;
  return results;
}
function applyReducer(document2, operation, index) {
  var operationResult = applyOperation(document2, operation);
  if (operationResult.test === false) {
    throw new JsonPatchError("Test operation failed", "TEST_OPERATION_FAILED", index, operation, document2);
  }
  return operationResult.newDocument;
}
function validator(operation, index, document2, existingPathFragment) {
  if (typeof operation !== "object" || operation === null || Array.isArray(operation)) {
    throw new JsonPatchError("Operation is not an object", "OPERATION_NOT_AN_OBJECT", index, operation, document2);
  } else if (!objOps[operation.op]) {
    throw new JsonPatchError("Operation `op` property is not one of operations defined in RFC-6902", "OPERATION_OP_INVALID", index, operation, document2);
  } else if (typeof operation.path !== "string") {
    throw new JsonPatchError("Operation `path` property is not a string", "OPERATION_PATH_INVALID", index, operation, document2);
  } else if (operation.path.indexOf("/") !== 0 && operation.path.length > 0) {
    throw new JsonPatchError('Operation `path` property must start with "/"', "OPERATION_PATH_INVALID", index, operation, document2);
  } else if ((operation.op === "move" || operation.op === "copy") && typeof operation.from !== "string") {
    throw new JsonPatchError("Operation `from` property is not present (applicable in `move` and `copy` operations)", "OPERATION_FROM_REQUIRED", index, operation, document2);
  } else if ((operation.op === "add" || operation.op === "replace" || operation.op === "test") && operation.value === void 0) {
    throw new JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)", "OPERATION_VALUE_REQUIRED", index, operation, document2);
  } else if ((operation.op === "add" || operation.op === "replace" || operation.op === "test") && hasUndefined(operation.value)) {
    throw new JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)", "OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED", index, operation, document2);
  } else if (document2) {
    if (operation.op == "add") {
      var pathLen = operation.path.split("/").length;
      var existingPathLen = existingPathFragment.split("/").length;
      if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) {
        throw new JsonPatchError("Cannot perform an `add` operation at the desired path", "OPERATION_PATH_CANNOT_ADD", index, operation, document2);
      }
    } else if (operation.op === "replace" || operation.op === "remove" || operation.op === "_get") {
      if (operation.path !== existingPathFragment) {
        throw new JsonPatchError("Cannot perform the operation at a path that does not exist", "OPERATION_PATH_UNRESOLVABLE", index, operation, document2);
      }
    } else if (operation.op === "move" || operation.op === "copy") {
      var existingValue = { op: "_get", path: operation.from, value: void 0 };
      var error = validate([existingValue], document2);
      if (error && error.name === "OPERATION_PATH_UNRESOLVABLE") {
        throw new JsonPatchError("Cannot perform the operation from a path that does not exist", "OPERATION_FROM_UNRESOLVABLE", index, operation, document2);
      }
    }
  }
}
function validate(sequence, document2, externalValidator) {
  try {
    if (!Array.isArray(sequence)) {
      throw new JsonPatchError("Patch sequence must be an array", "SEQUENCE_NOT_AN_ARRAY");
    }
    if (document2) {
      applyPatch(_deepClone(document2), _deepClone(sequence), externalValidator || true);
    } else {
      externalValidator = externalValidator || validator;
      for (var i = 0; i < sequence.length; i++) {
        externalValidator(sequence[i], i, document2, void 0);
      }
    }
  } catch (e) {
    if (e instanceof JsonPatchError) {
      return e;
    } else {
      throw e;
    }
  }
}
function _areEquals(a, b) {
  if (a === b)
    return true;
  if (a && b && typeof a == "object" && typeof b == "object") {
    var arrA = Array.isArray(a), arrB = Array.isArray(b), i, length, key;
    if (arrA && arrB) {
      length = a.length;
      if (length != b.length)
        return false;
      for (i = length; i-- !== 0; )
        if (!_areEquals(a[i], b[i]))
          return false;
      return true;
    }
    if (arrA != arrB)
      return false;
    var keys = Object.keys(a);
    length = keys.length;
    if (length !== Object.keys(b).length)
      return false;
    for (i = length; i-- !== 0; )
      if (!b.hasOwnProperty(keys[i]))
        return false;
    for (i = length; i-- !== 0; ) {
      key = keys[i];
      if (!_areEquals(a[key], b[key]))
        return false;
    }
    return true;
  }
  return a !== a && b !== b;
}
var core = /* @__PURE__ */ Object.freeze({
  __proto__: null,
  JsonPatchError,
  _areEquals,
  applyOperation,
  applyPatch,
  applyReducer,
  deepClone,
  getValueByPointer,
  validate,
  validator
});
/*!
 * https://github.com/Starcounter-Jack/JSON-Patch
 * (c) 2017-2021 Joachim Wester
 * MIT license
 */
var beforeDict = /* @__PURE__ */ new WeakMap();
var Mirror = (
  /** @class */
  /* @__PURE__ */ (function() {
    function Mirror2(obj) {
      this.observers = /* @__PURE__ */ new Map();
      this.obj = obj;
    }
    return Mirror2;
  })()
);
var ObserverInfo = (
  /** @class */
  /* @__PURE__ */ (function() {
    function ObserverInfo2(callback, observer) {
      this.callback = callback;
      this.observer = observer;
    }
    return ObserverInfo2;
  })()
);
function getMirror(obj) {
  return beforeDict.get(obj);
}
function getObserverFromMirror(mirror, callback) {
  return mirror.observers.get(callback);
}
function removeObserverFromMirror(mirror, observer) {
  mirror.observers.delete(observer.callback);
}
function unobserve(root, observer) {
  observer.unobserve();
}
function observe(obj, callback) {
  var patches = [];
  var observer;
  var mirror = getMirror(obj);
  if (!mirror) {
    mirror = new Mirror(obj);
    beforeDict.set(obj, mirror);
  } else {
    var observerInfo = getObserverFromMirror(mirror, callback);
    observer = observerInfo && observerInfo.observer;
  }
  if (observer) {
    return observer;
  }
  observer = {};
  mirror.value = _deepClone(obj);
  if (callback) {
    observer.callback = callback;
    observer.next = null;
    var dirtyCheck = function() {
      generate(observer);
    };
    var fastCheck = function() {
      clearTimeout(observer.next);
      observer.next = setTimeout(dirtyCheck);
    };
    if (typeof window !== "undefined") {
      window.addEventListener("mouseup", fastCheck);
      window.addEventListener("keyup", fastCheck);
      window.addEventListener("mousedown", fastCheck);
      window.addEventListener("keydown", fastCheck);
      window.addEventListener("change", fastCheck);
    }
  }
  observer.patches = patches;
  observer.object = obj;
  observer.unobserve = function() {
    generate(observer);
    clearTimeout(observer.next);
    removeObserverFromMirror(mirror, observer);
    if (typeof window !== "undefined") {
      window.removeEventListener("mouseup", fastCheck);
      window.removeEventListener("keyup", fastCheck);
      window.removeEventListener("mousedown", fastCheck);
      window.removeEventListener("keydown", fastCheck);
      window.removeEventListener("change", fastCheck);
    }
  };
  mirror.observers.set(callback, new ObserverInfo(callback, observer));
  return observer;
}
function generate(observer, invertible) {
  if (invertible === void 0) {
    invertible = false;
  }
  var mirror = beforeDict.get(observer.object);
  _generate(mirror.value, observer.object, observer.patches, "", invertible);
  if (observer.patches.length) {
    applyPatch(mirror.value, observer.patches);
  }
  var temp = observer.patches;
  if (temp.length > 0) {
    observer.patches = [];
    if (observer.callback) {
      observer.callback(temp);
    }
  }
  return temp;
}
function _generate(mirror, obj, patches, path, invertible) {
  if (obj === mirror) {
    return;
  }
  if (typeof obj.toJSON === "function") {
    obj = obj.toJSON();
  }
  var newKeys = _objectKeys(obj);
  var oldKeys = _objectKeys(mirror);
  var deleted = false;
  for (var t = oldKeys.length - 1; t >= 0; t--) {
    var key = oldKeys[t];
    var oldVal = mirror[key];
    if (hasOwnProperty(obj, key) && !(obj[key] === void 0 && oldVal !== void 0 && Array.isArray(obj) === false)) {
      var newVal = obj[key];
      if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) {
        _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible);
      } else {
        if (oldVal !== newVal) {
          if (invertible) {
            patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) });
          }
          patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) });
        }
      }
    } else if (Array.isArray(mirror) === Array.isArray(obj)) {
      if (invertible) {
        patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) });
      }
      patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) });
      deleted = true;
    } else {
      if (invertible) {
        patches.push({ op: "test", path, value: mirror });
      }
      patches.push({ op: "replace", path, value: obj });
    }
  }
  if (!deleted && newKeys.length == oldKeys.length) {
    return;
  }
  for (var t = 0; t < newKeys.length; t++) {
    var key = newKeys[t];
    if (!hasOwnProperty(mirror, key) && obj[key] !== void 0) {
      patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: _deepClone(obj[key]) });
    }
  }
}
function compare(tree1, tree2, invertible) {
  if (invertible === void 0) {
    invertible = false;
  }
  var patches = [];
  _generate(tree1, tree2, patches, "", invertible);
  return patches;
}
var duplex = /* @__PURE__ */ Object.freeze({
  __proto__: null,
  compare,
  generate,
  observe,
  unobserve
});
Object.assign({}, core, duplex, {
  JsonPatchError: PatchError,
  deepClone: _deepClone,
  escapePathComponent,
  unescapePathComponent
});
function getDefaultExportFromCjs(x) {
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
}
var lrucache;
var hasRequiredLrucache;
function requireLrucache() {
  if (hasRequiredLrucache) return lrucache;
  hasRequiredLrucache = 1;
  class LRUCache {
    constructor() {
      this.max = 1e3;
      this.map = /* @__PURE__ */ new Map();
    }
    get(key) {
      const value = this.map.get(key);
      if (value === void 0) {
        return void 0;
      } else {
        this.map.delete(key);
        this.map.set(key, value);
        return value;
      }
    }
    delete(key) {
      return this.map.delete(key);
    }
    set(key, value) {
      const deleted = this.delete(key);
      if (!deleted && value !== void 0) {
        if (this.map.size >= this.max) {
          const firstKey = this.map.keys().next().value;
          this.delete(firstKey);
        }
        this.map.set(key, value);
      }
      return this;
    }
  }
  lrucache = LRUCache;
  return lrucache;
}
var parseOptions_1;
var hasRequiredParseOptions;
function requireParseOptions() {
  if (hasRequiredParseOptions) return parseOptions_1;
  hasRequiredParseOptions = 1;
  const looseOption = Object.freeze({ loose: true });
  const emptyOpts = Object.freeze({});
  const parseOptions = (options) => {
    if (!options) {
      return emptyOpts;
    }
    if (typeof options !== "object") {
      return looseOption;
    }
    return options;
  };
  parseOptions_1 = parseOptions;
  return parseOptions_1;
}
var re = { exports: {} };
var constants;
var hasRequiredConstants;
function requireConstants() {
  if (hasRequiredConstants) return constants;
  hasRequiredConstants = 1;
  const SEMVER_SPEC_VERSION = "2.0.0";
  const MAX_LENGTH = 256;
  const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || /* istanbul ignore next */
  9007199254740991;
  const MAX_SAFE_COMPONENT_LENGTH = 16;
  const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6;
  const RELEASE_TYPES = [
    "major",
    "premajor",
    "minor",
    "preminor",
    "patch",
    "prepatch",
    "prerelease"
  ];
  constants = {
    MAX_LENGTH,
    MAX_SAFE_COMPONENT_LENGTH,
    MAX_SAFE_BUILD_LENGTH,
    MAX_SAFE_INTEGER,
    RELEASE_TYPES,
    SEMVER_SPEC_VERSION,
    FLAG_INCLUDE_PRERELEASE: 1,
    FLAG_LOOSE: 2
  };
  return constants;
}
var debug_1;
var hasRequiredDebug;
function requireDebug() {
  if (hasRequiredDebug) return debug_1;
  hasRequiredDebug = 1;
  const debug = typeof process === "object" && define_process_env_default && define_process_env_default.NODE_DEBUG && /\bsemver\b/i.test(define_process_env_default.NODE_DEBUG) ? (...args) => console.error("SEMVER", ...args) : () => {
  };
  debug_1 = debug;
  return debug_1;
}
var hasRequiredRe;
function requireRe() {
  if (hasRequiredRe) return re.exports;
  hasRequiredRe = 1;
  (function(module, exports$1) {
    const {
      MAX_SAFE_COMPONENT_LENGTH,
      MAX_SAFE_BUILD_LENGTH,
      MAX_LENGTH
    } = requireConstants();
    const debug = requireDebug();
    exports$1 = module.exports = {};
    const re2 = exports$1.re = [];
    const safeRe = exports$1.safeRe = [];
    const src = exports$1.src = [];
    const safeSrc = exports$1.safeSrc = [];
    const t = exports$1.t = {};
    let R = 0;
    const LETTERDASHNUMBER = "[a-zA-Z0-9-]";
    const safeRegexReplacements = [
      ["\\s", 1],
      ["\\d", MAX_LENGTH],
      [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH]
    ];
    const makeSafeRegex = (value) => {
      for (const [token, max] of safeRegexReplacements) {
        value = value.split(`${token}*`).join(`${token}{0,${max}}`).split(`${token}+`).join(`${token}{1,${max}}`);
      }
      return value;
    };
    const createToken = (name, value, isGlobal) => {
      const safe = makeSafeRegex(value);
      const index = R++;
      debug(name, index, value);
      t[name] = index;
      src[index] = value;
      safeSrc[index] = safe;
      re2[index] = new RegExp(value, isGlobal ? "g" : void 0);
      safeRe[index] = new RegExp(safe, isGlobal ? "g" : void 0);
    };
    createToken("NUMERICIDENTIFIER", "0|[1-9]\\d*");
    createToken("NUMERICIDENTIFIERLOOSE", "\\d+");
    createToken("NONNUMERICIDENTIFIER", `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`);
    createToken("MAINVERSION", `(${src[t.NUMERICIDENTIFIER]})\\.(${src[t.NUMERICIDENTIFIER]})\\.(${src[t.NUMERICIDENTIFIER]})`);
    createToken("MAINVERSIONLOOSE", `(${src[t.NUMERICIDENTIFIERLOOSE]})\\.(${src[t.NUMERICIDENTIFIERLOOSE]})\\.(${src[t.NUMERICIDENTIFIERLOOSE]})`);
    createToken("PRERELEASEIDENTIFIER", `(?:${src[t.NONNUMERICIDENTIFIER]}|${src[t.NUMERICIDENTIFIER]})`);
    createToken("PRERELEASEIDENTIFIERLOOSE", `(?:${src[t.NONNUMERICIDENTIFIER]}|${src[t.NUMERICIDENTIFIERLOOSE]})`);
    createToken("PRERELEASE", `(?:-(${src[t.PRERELEASEIDENTIFIER]}(?:\\.${src[t.PRERELEASEIDENTIFIER]})*))`);
    createToken("PRERELEASELOOSE", `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE]}(?:\\.${src[t.PRERELEASEIDENTIFIERLOOSE]})*))`);
    createToken("BUILDIDENTIFIER", `${LETTERDASHNUMBER}+`);
    createToken("BUILD", `(?:\\+(${src[t.BUILDIDENTIFIER]}(?:\\.${src[t.BUILDIDENTIFIER]})*))`);
    createToken("FULLPLAIN", `v?${src[t.MAINVERSION]}${src[t.PRERELEASE]}?${src[t.BUILD]}?`);
    createToken("FULL", `^${src[t.FULLPLAIN]}$`);
    createToken("LOOSEPLAIN", `[v=\\s]*${src[t.MAINVERSIONLOOSE]}${src[t.PRERELEASELOOSE]}?${src[t.BUILD]}?`);
    createToken("LOOSE", `^${src[t.LOOSEPLAIN]}$`);
    createToken("GTLT", "((?:<|>)?=?)");
    createToken("XRANGEIDENTIFIERLOOSE", `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);
    createToken("XRANGEIDENTIFIER", `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`);
    createToken("XRANGEPLAIN", `[v=\\s]*(${src[t.XRANGEIDENTIFIER]})(?:\\.(${src[t.XRANGEIDENTIFIER]})(?:\\.(${src[t.XRANGEIDENTIFIER]})(?:${src[t.PRERELEASE]})?${src[t.BUILD]}?)?)?`);
    createToken("XRANGEPLAINLOOSE", `[v=\\s]*(${src[t.XRANGEIDENTIFIERLOOSE]})(?:\\.(${src[t.XRANGEIDENTIFIERLOOSE]})(?:\\.(${src[t.XRANGEIDENTIFIERLOOSE]})(?:${src[t.PRERELEASELOOSE]})?${src[t.BUILD]}?)?)?`);
    createToken("XRANGE", `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAIN]}$`);
    createToken("XRANGELOOSE", `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`);
    createToken("COERCEPLAIN", `${"(^|[^\\d])(\\d{1,"}${MAX_SAFE_COMPONENT_LENGTH}})(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`);
    createToken("COERCE", `${src[t.COERCEPLAIN]}(?:$|[^\\d])`);
    createToken("COERCEFULL", src[t.COERCEPLAIN] + `(?:${src[t.PRERELEASE]})?(?:${src[t.BUILD]})?(?:$|[^\\d])`);
    createToken("COERCERTL", src[t.COERCE], true);
    createToken("COERCERTLFULL", src[t.COERCEFULL], true);
    createToken("LONETILDE", "(?:~>?)");
    createToken("TILDETRIM", `(\\s*)${src[t.LONETILDE]}\\s+`, true);
    exports$1.tildeTrimReplace = "$1~";
    createToken("TILDE", `^${src[t.LONETILDE]}${src[t.XRANGEPLAIN]}$`);
    createToken("TILDELOOSE", `^${src[t.LONETILDE]}${src[t.XRANGEPLAINLOOSE]}$`);
    createToken("LONECARET", "(?:\\^)");
    createToken("CARETTRIM", `(\\s*)${src[t.LONECARET]}\\s+`, true);
    exports$1.caretTrimReplace = "$1^";
    createToken("CARET", `^${src[t.LONECARET]}${src[t.XRANGEPLAIN]}$`);
    createToken("CARETLOOSE", `^${src[t.LONECARET]}${src[t.XRANGEPLAINLOOSE]}$`);
    createToken("COMPARATORLOOSE", `^${src[t.GTLT]}\\s*(${src[t.LOOSEPLAIN]})$|^$`);
    createToken("COMPARATOR", `^${src[t.GTLT]}\\s*(${src[t.FULLPLAIN]})$|^$`);
    createToken("COMPARATORTRIM", `(\\s*)${src[t.GTLT]}\\s*(${src[t.LOOSEPLAIN]}|${src[t.XRANGEPLAIN]})`, true);
    exports$1.comparatorTrimReplace = "$1$2$3";
    createToken("HYPHENRANGE", `^\\s*(${src[t.XRANGEPLAIN]})\\s+-\\s+(${src[t.XRANGEPLAIN]})\\s*$`);
    createToken("HYPHENRANGELOOSE", `^\\s*(${src[t.XRANGEPLAINLOOSE]})\\s+-\\s+(${src[t.XRANGEPLAINLOOSE]})\\s*$`);
    createToken("STAR", "(<|>)?=?\\s*\\*");
    createToken("GTE0", "^\\s*>=\\s*0\\.0\\.0\\s*$");
    createToken("GTE0PRE", "^\\s*>=\\s*0\\.0\\.0-0\\s*$");
  })(re, re.exports);
  return re.exports;
}
var identifiers;
var hasRequiredIdentifiers;
function requireIdentifiers() {
  if (hasRequiredIdentifiers) return identifiers;
  hasRequiredIdentifiers = 1;
  const numeric = /^[0-9]+$/;
  const compareIdentifiers = (a, b) => {
    const anum = numeric.test(a);
    const bnum = numeric.test(b);
    if (anum && bnum) {
      a = +a;
      b = +b;
    }
    return a === b ? 0 : anum && !bnum ? -1 : bnum && !anum ? 1 : a < b ? -1 : 1;
  };
  const rcompareIdentifiers = (a, b) => compareIdentifiers(b, a);
  identifiers = {
    compareIdentifiers,
    rcompareIdentifiers
  };
  return identifiers;
}
var semver;
var hasRequiredSemver;
function requireSemver() {
  if (hasRequiredSemver) return semver;
  hasRequiredSemver = 1;
  const debug = requireDebug();
  const { MAX_LENGTH, MAX_SAFE_INTEGER } = requireConstants();
  const { safeRe: re2, t } = requireRe();
  const parseOptions = requireParseOptions();
  const { compareIdentifiers } = requireIdentifiers();
  class SemVer {
    constructor(version2, options) {
      options = parseOptions(options);
      if (version2 instanceof SemVer) {
        if (version2.loose === !!options.loose && version2.includePrerelease === !!options.includePrerelease) {
          return version2;
        } else {
          version2 = version2.version;
        }
      } else if (typeof version2 !== "string") {
        throw new TypeError(`Invalid version. Must be a string. Got type "${typeof version2}".`);
      }
      if (version2.length > MAX_LENGTH) {
        throw new TypeError(
          `version is longer than ${MAX_LENGTH} characters`
        );
      }
      debug("SemVer", version2, options);
      this.options = options;
      this.loose = !!options.loose;
      this.includePrerelease = !!options.includePrerelease;
      const m = version2.trim().match(options.loose ? re2[t.LOOSE] : re2[t.FULL]);
      if (!m) {
        throw new TypeError(`Invalid Version: ${version2}`);
      }
      this.raw = version2;
      this.major = +m[1];
      this.minor = +m[2];
      this.patch = +m[3];
      if (this.major > MAX_SAFE_INTEGER || this.major < 0) {
        throw new TypeError("Invalid major version");
      }
      if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) {
        throw new TypeError("Invalid minor version");
      }
      if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) {
        throw new TypeError("Invalid patch version");
      }
      if (!m[4]) {
        this.prerelease = [];
      } else {
        this.prerelease = m[4].split(".").map((id) => {
          if (/^[0-9]+$/.test(id)) {
            const num = +id;
            if (num >= 0 && num < MAX_SAFE_INTEGER) {
              return num;
            }
          }
          return id;
        });
      }
      this.build = m[5] ? m[5].split(".") : [];
      this.format();
    }
    format() {
      this.version = `${this.major}.${this.minor}.${this.patch}`;
      if (this.prerelease.length) {
        this.version += `-${this.prerelease.join(".")}`;
      }
      return this.version;
    }
    toString() {
      return this.version;
    }
    compare(other) {
      debug("SemVer.compare", this.version, this.options, other);
      if (!(other instanceof SemVer)) {
        if (typeof other === "string" && other === this.version) {
          return 0;
        }
        other = new SemVer(other, this.options);
      }
      if (other.version === this.version) {
        return 0;
      }
      return this.compareMain(other) || this.comparePre(other);
    }
    compareMain(other) {
      if (!(other instanceof SemVer)) {
        other = new SemVer(other, this.options);
      }
      return compareIdentifiers(this.major, other.major) || compareIdentifiers(this.minor, other.minor) || compareIdentifiers(this.patch, other.patch);
    }
    comparePre(other) {
      if (!(other instanceof SemVer)) {
        other = new SemVer(other, this.options);
      }
      if (this.prerelease.length && !other.prerelease.length) {
        return -1;
      } else if (!this.prerelease.length && other.prerelease.length) {
        return 1;
      } else if (!this.prerelease.length && !other.prerelease.length) {
        return 0;
      }
      let i = 0;
      do {
        const a = this.prerelease[i];
        const b = other.prerelease[i];
        debug("prerelease compare", i, a, b);
        if (a === void 0 && b === void 0) {
          return 0;
        } else if (b === void 0) {
          return 1;
        } else if (a === void 0) {
          return -1;
        } else if (a === b) {
          continue;
        } else {
          return compareIdentifiers(a, b);
        }
      } while (++i);
    }
    compareBuild(other) {
      if (!(other instanceof SemVer)) {
        other = new SemVer(other, this.options);
      }
      let i = 0;
      do {
        const a = this.build[i];
        const b = other.build[i];
        debug("build compare", i, a, b);
        if (a === void 0 && b === void 0) {
          return 0;
        } else if (b === void 0) {
          return 1;
        } else if (a === void 0) {
          return -1;
        } else if (a === b) {
          continue;
        } else {
          return compareIdentifiers(a, b);
        }
      } while (++i);
    }
    // preminor will bump the version up to the next minor release, and immediately
    // down to pre-release. premajor and prepatch work the same way.
    inc(release, identifier, identifierBase) {
      if (release.startsWith("pre")) {
        if (!identifier && identifierBase === false) {
          throw new Error("invalid increment argument: identifier is empty");
        }
        if (identifier) {
          const match = `-${identifier}`.match(this.options.loose ? re2[t.PRERELEASELOOSE] : re2[t.PRERELEASE]);
          if (!match || match[1] !== identifier) {
            throw new Error(`invalid identifier: ${identifier}`);
          }
        }
      }
      switch (release) {
        case "premajor":
          this.prerelease.length = 0;
          this.patch = 0;
          this.minor = 0;
          this.major++;
          this.inc("pre", identifier, identifierBase);
          break;
        case "preminor":
          this.prerelease.length = 0;
          this.patch = 0;
          this.minor++;
          this.inc("pre", identifier, identifierBase);
          break;
        case "prepatch":
          this.prerelease.length = 0;
          this.inc("patch", identifier, identifierBase);
          this.inc("pre", identifier, identifierBase);
          break;
        // If the input is a non-prerelease version, this acts the same as
        // prepatch.
        case "prerelease":
          if (this.prerelease.length === 0) {
            this.inc("patch", identifier, identifierBase);
          }
          this.inc("pre", identifier, identifierBase);
          break;
        case "release":
          if (this.prerelease.length === 0) {
            throw new Error(`version ${this.raw} is not a prerelease`);
          }
          this.prerelease.length = 0;
          break;
        case "major":
          if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) {
            this.major++;
          }
          this.minor = 0;
          this.patch = 0;
          this.prerelease = [];
          break;
        case "minor":
          if (this.patch !== 0 || this.prerelease.length === 0) {
            this.minor++;
          }
          this.patch = 0;
          this.prerelease = [];
          break;
        case "patch":
          if (this.prerelease.length === 0) {
            this.patch++;
          }
          this.prerelease = [];
          break;
        // This probably shouldn't be used publicly.
        // 1.0.0 'pre' would become 1.0.0-0 which is the wrong direction.
        case "pre": {
          const base = Number(identifierBase) ? 1 : 0;
          if (this.prerelease.length === 0) {
            this.prerelease = [base];
          } else {
            let i = this.prerelease.length;
            while (--i >= 0) {
              if (typeof this.prerelease[i] === "number") {
                this.prerelease[i]++;
                i = -2;
              }
            }
            if (i === -1) {
              if (identifier === this.prerelease.join(".") && identifierBase === false) {
                throw new Error("invalid increment argument: identifier already exists");
              }
              this.prerelease.push(base);
            }
          }
          if (identifier) {
            let prerelease = [identifier, base];
            if (identifierBase === false) {
              prerelease = [identifier];
            }
            if (compareIdentifiers(this.prerelease[0], identifier) === 0) {
              if (isNaN(this.prerelease[1])) {
                this.prerelease = prerelease;
              }
            } else {
              this.prerelease = prerelease;
            }
          }
          break;
        }
        default:
          throw new Error(`invalid increment argument: ${release}`);
      }
      this.raw = this.format();
      if (this.build.length) {
        this.raw += `+${this.build.join(".")}`;
      }
      return this;
    }
  }
  semver = SemVer;
  return semver;
}
var compare_1;
var hasRequiredCompare;
function requireCompare() {
  if (hasRequiredCompare) return compare_1;
  hasRequiredCompare = 1;
  const SemVer = requireSemver();
  const compare2 = (a, b, loose) => new SemVer(a, loose).compare(new SemVer(b, loose));
  compare_1 = compare2;
  return compare_1;
}
var eq_1;
var hasRequiredEq;
function requireEq() {
  if (hasRequiredEq) return eq_1;
  hasRequiredEq = 1;
  const compare2 = requireCompare();
  const eq = (a, b, loose) => compare2(a, b, loose) === 0;
  eq_1 = eq;
  return eq_1;
}
var neq_1;
var hasRequiredNeq;
function requireNeq() {
  if (hasRequiredNeq) return neq_1;
  hasRequiredNeq = 1;
  const compare2 = requireCompare();
  const neq = (a, b, loose) => compare2(a, b, loose) !== 0;
  neq_1 = neq;
  return neq_1;
}
var gt_1;
var hasRequiredGt;
function requireGt() {
  if (hasRequiredGt) return gt_1;
  hasRequiredGt = 1;
  const compare2 = requireCompare();
  const gt = (a, b, loose) => compare2(a, b, loose) > 0;
  gt_1 = gt;
  return gt_1;
}
var gte_1;
var hasRequiredGte;
function requireGte() {
  if (hasRequiredGte) return gte_1;
  hasRequiredGte = 1;
  const compare2 = requireCompare();
  const gte = (a, b, loose) => compare2(a, b, loose) >= 0;
  gte_1 = gte;
  return gte_1;
}
var lt_1;
var hasRequiredLt;
function requireLt() {
  if (hasRequiredLt) return lt_1;
  hasRequiredLt = 1;
  const compare2 = requireCompare();
  const lt = (a, b, loose) => compare2(a, b, loose) < 0;
  lt_1 = lt;
  return lt_1;
}
var lte_1;
var hasRequiredLte;
function requireLte() {
  if (hasRequiredLte) return lte_1;
  hasRequiredLte = 1;
  const compare2 = requireCompare();
  const lte = (a, b, loose) => compare2(a, b, loose) <= 0;
  lte_1 = lte;
  return lte_1;
}
var cmp_1;
var hasRequiredCmp;
function requireCmp() {
  if (hasRequiredCmp) return cmp_1;
  hasRequiredCmp = 1;
  const eq = requireEq();
  const neq = requireNeq();
  const gt = requireGt();
  const gte = requireGte();
  const lt = requireLt();
  const lte = requireLte();
  const cmp = (a, op, b, loose) => {
    switch (op) {
      case "===":
        if (typeof a === "object") {
          a = a.version;
        }
        if (typeof b === "object") {
          b = b.version;
        }
        return a === b;
      case "!==":
        if (typeof a === "object") {
          a = a.version;
        }
        if (typeof b === "object") {
          b = b.version;
        }
        return a !== b;
      case "":
      case "=":
      case "==":
        return eq(a, b, loose);
      case "!=":
        return neq(a, b, loose);
      case ">":
        return gt(a, b, loose);
      case ">=":
        return gte(a, b, loose);
      case "<":
        return lt(a, b, loose);
      case "<=":
        return lte(a, b, loose);
      default:
        throw new TypeError(`Invalid operator: ${op}`);
    }
  };
  cmp_1 = cmp;
  return cmp_1;
}
var comparator;
var hasRequiredComparator;
function requireComparator() {
  if (hasRequiredComparator) return comparator;
  hasRequiredComparator = 1;
  const ANY = /* @__PURE__ */ Symbol("SemVer ANY");
  class Comparator {
    static get ANY() {
      return ANY;
    }
    constructor(comp, options) {
      options = parseOptions(options);
      if (comp instanceof Comparator) {
        if (comp.loose === !!options.loose) {
          return comp;
        } else {
          comp = comp.value;
        }
      }
      comp = comp.trim().split(/\s+/).join(" ");
      debug("comparator", comp, options);
      this.options = options;
      this.loose = !!options.loose;
      this.parse(comp);
      if (this.semver === ANY) {
        this.value = "";
      } else {
        this.value = this.operator + this.semver.version;
      }
      debug("comp", this);
    }
    parse(comp) {
      const r = this.options.loose ? re2[t.COMPARATORLOOSE] : re2[t.COMPARATOR];
      const m = comp.match(r);
      if (!m) {
        throw new TypeError(`Invalid comparator: ${comp}`);
      }
      this.operator = m[1] !== void 0 ? m[1] : "";
      if (this.operator === "=") {
        this.operator = "";
      }
      if (!m[2]) {
        this.semver = ANY;
      } else {
        this.semver = new SemVer(m[2], this.options.loose);
      }
    }
    toString() {
      return this.value;
    }
    test(version2) {
      debug("Comparator.test", version2, this.options.loose);
      if (this.semver === ANY || version2 === ANY) {
        return true;
      }
      if (typeof version2 === "string") {
        try {
          version2 = new SemVer(version2, this.options);
        } catch (er) {
          return false;
        }
      }
      return cmp(version2, this.operator, this.semver, this.options);
    }
    intersects(comp, options) {
      if (!(comp instanceof Comparator)) {
        throw new TypeError("a Comparator is required");
      }
      if (this.operator === "") {
        if (this.value === "") {
          return true;
        }
        return new Range(comp.value, options).test(this.value);
      } else if (comp.operator === "") {
        if (comp.value === "") {
          return true;
        }
        return new Range(this.value, options).test(comp.semver);
      }
      options = parseOptions(options);
      if (options.includePrerelease && (this.value === "<0.0.0-0" || comp.value === "<0.0.0-0")) {
        return false;
      }
      if (!options.includePrerelease && (this.value.startsWith("<0.0.0") || comp.value.startsWith("<0.0.0"))) {
        return false;
      }
      if (this.operator.startsWith(">") && comp.operator.startsWith(">")) {
        return true;
      }
      if (this.operator.startsWith("<") && comp.operator.startsWith("<")) {
        return true;
      }
      if (this.semver.version === comp.semver.version && this.operator.includes("=") && comp.operator.includes("=")) {
        return true;
      }
      if (cmp(this.semver, "<", comp.semver, options) && this.operator.startsWith(">") && comp.operator.startsWith("<")) {
        return true;
      }
      if (cmp(this.semver, ">", comp.semver, options) && this.operator.startsWith("<") && comp.operator.startsWith(">")) {
        return true;
      }
      return false;
    }
  }
  comparator = Comparator;
  const parseOptions = requireParseOptions();
  const { safeRe: re2, t } = requireRe();
  const cmp = requireCmp();
  const debug = requireDebug();
  const SemVer = requireSemver();
  const Range = requireRange();
  return comparator;
}
var range;
var hasRequiredRange;
function requireRange() {
  if (hasRequiredRange) return range;
  hasRequiredRange = 1;
  const SPACE_CHARACTERS = /\s+/g;
  class Range {
    constructor(range2, options) {
      options = parseOptions(options);
      if (range2 instanceof Range) {
        if (range2.loose === !!options.loose && range2.includePrerelease === !!options.includePrerelease) {
          return range2;
        } else {
          return new Range(range2.raw, options);
        }
      }
      if (range2 instanceof Comparator) {
        this.raw = range2.value;
        this.set = [[range2]];
        this.formatted = void 0;
        return this;
      }
      this.options = options;
      this.loose = !!options.loose;
      this.includePrerelease = !!options.includePrerelease;
      this.raw = range2.trim().replace(SPACE_CHARACTERS, " ");
      this.set = this.raw.split("||").map((r) => this.parseRange(r.trim())).filter((c) => c.length);
      if (!this.set.length) {
        throw new TypeError(`Invalid SemVer Range: ${this.raw}`);
      }
      if (this.set.length > 1) {
        const first = this.set[0];
        this.set = this.set.filter((c) => !isNullSet(c[0]));
        if (this.set.length === 0) {
          this.set = [first];
        } else if (this.set.length > 1) {
          for (const c of this.set) {
            if (c.length === 1 && isAny(c[0])) {
              this.set = [c];
              break;
            }
          }
        }
      }
      this.formatted = void 0;
    }
    get range() {
      if (this.formatted === void 0) {
        this.formatted = "";
        for (let i = 0; i < this.set.length; i++) {
          if (i > 0) {
            this.formatted += "||";
          }
          const comps = this.set[i];
          for (let k = 0; k < comps.length; k++) {
            if (k > 0) {
              this.formatted += " ";
            }
            this.formatted += comps[k].toString().trim();
          }
        }
      }
      return this.formatted;
    }
    format() {
      return this.range;
    }
    toString() {
      return this.range;
    }
    parseRange(range2) {
      const memoOpts = (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | (this.options.loose && FLAG_LOOSE);
      const memoKey = memoOpts + ":" + range2;
      const cached = cache.get(memoKey);
      if (cached) {
        return cached;
      }
      const loose = this.options.loose;
      const hr = loose ? re2[t.HYPHENRANGELOOSE] : re2[t.HYPHENRANGE];
      range2 = range2.replace(hr, hyphenReplace(this.options.includePrerelease));
      debug("hyphen replace", range2);
      range2 = range2.replace(re2[t.COMPARATORTRIM], comparatorTrimReplace);
      debug("comparator trim", range2);
      range2 = range2.replace(re2[t.TILDETRIM], tildeTrimReplace);
      debug("tilde trim", range2);
      range2 = range2.replace(re2[t.CARETTRIM], caretTrimReplace);
      debug("caret trim", range2);
      let rangeList = range2.split(" ").map((comp) => parseComparator(comp, this.options)).join(" ").split(/\s+/).map((comp) => replaceGTE0(comp, this.options));
      if (loose) {
        rangeList = rangeList.filter((comp) => {
          debug("loose invalid filter", comp, this.options);
          return !!comp.match(re2[t.COMPARATORLOOSE]);
        });
      }
      debug("range list", rangeList);
      const rangeMap = /* @__PURE__ */ new Map();
      const comparators = rangeList.map((comp) => new Comparator(comp, this.options));
      for (const comp of comparators) {
        if (isNullSet(comp)) {
          return [comp];
        }
        rangeMap.set(comp.value, comp);
      }
      if (rangeMap.size > 1 && rangeMap.has("")) {
        rangeMap.delete("");
      }
      const result = [...rangeMap.values()];
      cache.set(memoKey, result);
      return result;
    }
    intersects(range2, options) {
      if (!(range2 instanceof Range)) {
        throw new TypeError("a Range is required");
      }
      return this.set.some((thisComparators) => {
        return isSatisfiable(thisComparators, options) && range2.set.some((rangeComparators) => {
          return isSatisfiable(rangeComparators, options) && thisComparators.every((thisComparator) => {
            return rangeComparators.every((rangeComparator) => {
              return thisComparator.intersects(rangeComparator, options);
            });
          });
        });
      });
    }
    // if ANY of the sets match ALL of its comparators, then pass
    test(version2) {
      if (!version2) {
        return false;
      }
      if (typeof version2 === "string") {
        try {
          version2 = new SemVer(version2, this.options);
        } catch (er) {
          return false;
        }
      }
      for (let i = 0; i < this.set.length; i++) {
        if (testSet(this.set[i], version2, this.options)) {
          return true;
        }
      }
      return false;
    }
  }
  range = Range;
  const LRU = requireLrucache();
  const cache = new LRU();
  const parseOptions = requireParseOptions();
  const Comparator = requireComparator();
  const debug = requireDebug();
  const SemVer = requireSemver();
  const {
    safeRe: re2,
    t,
    comparatorTrimReplace,
    tildeTrimReplace,
    caretTrimReplace
  } = requireRe();
  const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = requireConstants();
  const isNullSet = (c) => c.value === "<0.0.0-0";
  const isAny = (c) => c.value === "";
  const isSatisfiable = (comparators, options) => {
    let result = true;
    const remainingComparators = comparators.slice();
    let testComparator = remainingComparators.pop();
    while (result && remainingComparators.length) {
      result = remainingComparators.every((otherComparator) => {
        return testComparator.intersects(otherComparator, options);
      });
      testComparator = remainingComparators.pop();
    }
    return result;
  };
  const parseComparator = (comp, options) => {
    debug("comp", comp, options);
    comp = replaceCarets(comp, options);
    debug("caret", comp);
    comp = replaceTildes(comp, options);
    debug("tildes", comp);
    comp = replaceXRanges(comp, options);
    debug("xrange", comp);
    comp = replaceStars(comp, options);
    debug("stars", comp);
    return comp;
  };
  const isX = (id) => !id || id.toLowerCase() === "x" || id === "*";
  const replaceTildes = (comp, options) => {
    return comp.trim().split(/\s+/).map((c) => replaceTilde(c, options)).join(" ");
  };
  const replaceTilde = (comp, options) => {
    const r = options.loose ? re2[t.TILDELOOSE] : re2[t.TILDE];
    return comp.replace(r, (_, M, m, p, pr) => {
      debug("tilde", comp, _, M, m, p, pr);
      let ret;
      if (isX(M)) {
        ret = "";
      } else if (isX(m)) {
        ret = `>=${M}.0.0 <${+M + 1}.0.0-0`;
      } else if (isX(p)) {
        ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`;
      } else if (pr) {
        debug("replaceTilde pr", pr);
        ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
      } else {
        ret = `>=${M}.${m}.${p} <${M}.${+m + 1}.0-0`;
      }
      debug("tilde return", ret);
      return ret;
    });
  };
  const replaceCarets = (comp, options) => {
    return comp.trim().split(/\s+/).map((c) => replaceCaret(c, options)).join(" ");
  };
  const replaceCaret = (comp, options) => {
    debug("caret", comp, options);
    const r = options.loose ? re2[t.CARETLOOSE] : re2[t.CARET];
    const z = options.includePrerelease ? "-0" : "";
    return comp.replace(r, (_, M, m, p, pr) => {
      debug("caret", comp, _, M, m, p, pr);
      let ret;
      if (isX(M)) {
        ret = "";
      } else if (isX(m)) {
        ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0`;
      } else if (isX(p)) {
        if (M === "0") {
          ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`;
        } else {
          ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`;
        }
      } else if (pr) {
        debug("replaceCaret pr", pr);
        if (M === "0") {
          if (m === "0") {
            ret = `>=${M}.${m}.${p}-${pr} <${M}.${m}.${+p + 1}-0`;
          } else {
            ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
          }
        } else {
          ret = `>=${M}.${m}.${p}-${pr} <${+M + 1}.0.0-0`;
        }
      } else {
        debug("no pr");
        if (M === "0") {
          if (m === "0") {
            ret = `>=${M}.${m}.${p}${z} <${M}.${m}.${+p + 1}-0`;
          } else {
            ret = `>=${M}.${m}.${p}${z} <${M}.${+m + 1}.0-0`;
          }
        } else {
          ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
        }
      }
      debug("caret return", ret);
      return ret;
    });
  };
  const replaceXRanges = (comp, options) => {
    debug("replaceXRanges", comp, options);
    return comp.split(/\s+/).map((c) => replaceXRange(c, options)).join(" ");
  };
  const replaceXRange = (comp, options) => {
    comp = comp.trim();
    const r = options.loose ? re2[t.XRANGELOOSE] : re2[t.XRANGE];
    return comp.replace(r, (ret, gtlt, M, m, p, pr) => {
      debug("xRange", comp, ret, gtlt, M, m, p, pr);
      const xM = isX(M);
      const xm = xM || isX(m);
      const xp = xm || isX(p);
      const anyX = xp;
      if (gtlt === "=" && anyX) {
        gtlt = "";
      }
      pr = options.includePrerelease ? "-0" : "";
      if (xM) {
        if (gtlt === ">" || gtlt === "<") {
          ret = "<0.0.0-0";
        } else {
          ret = "*";
        }
      } else if (gtlt && anyX) {
        if (xm) {
          m = 0;
        }
        p = 0;
        if (gtlt === ">") {
          gtlt = ">=";
          if (xm) {
            M = +M + 1;
            m = 0;
            p = 0;
          } else {
            m = +m + 1;
            p = 0;
          }
        } else if (gtlt === "<=") {
          gtlt = "<";
          if (xm) {
            M = +M + 1;
          } else {
            m = +m + 1;
          }
        }
        if (gtlt === "<") {
          pr = "-0";
        }
        ret = `${gtlt + M}.${m}.${p}${pr}`;
      } else if (xm) {
        ret = `>=${M}.0.0${pr} <${+M + 1}.0.0-0`;
      } else if (xp) {
        ret = `>=${M}.${m}.0${pr} <${M}.${+m + 1}.0-0`;
      }
      debug("xRange return", ret);
      return ret;
    });
  };
  const replaceStars = (comp, options) => {
    debug("replaceStars", comp, options);
    return comp.trim().replace(re2[t.STAR], "");
  };
  const replaceGTE0 = (comp, options) => {
    debug("replaceGTE0", comp, options);
    return comp.trim().replace(re2[options.includePrerelease ? t.GTE0PRE : t.GTE0], "");
  };
  const hyphenReplace = (incPr) => ($0, from, fM, fm, fp, fpr, fb, to, tM, tm, tp, tpr) => {
    if (isX(fM)) {
      from = "";
    } else if (isX(fm)) {
      from = `>=${fM}.0.0${incPr ? "-0" : ""}`;
    } else if (isX(fp)) {
      from = `>=${fM}.${fm}.0${incPr ? "-0" : ""}`;
    } else if (fpr) {
      from = `>=${from}`;
    } else {
      from = `>=${from}${incPr ? "-0" : ""}`;
    }
    if (isX(tM)) {
      to = "";
    } else if (isX(tm)) {
      to = `<${+tM + 1}.0.0-0`;
    } else if (isX(tp)) {
      to = `<${tM}.${+tm + 1}.0-0`;
    } else if (tpr) {
      to = `<=${tM}.${tm}.${tp}-${tpr}`;
    } else if (incPr) {
      to = `<${tM}.${tm}.${+tp + 1}-0`;
    } else {
      to = `<=${to}`;
    }
    return `${from} ${to}`.trim();
  };
  const testSet = (set, version2, options) => {
    for (let i = 0; i < set.length; i++) {
      if (!set[i].test(version2)) {
        return false;
      }
    }
    if (version2.prerelease.length && !options.includePrerelease) {
      for (let i = 0; i < set.length; i++) {
        debug(set[i].semver);
        if (set[i].semver === Comparator.ANY) {
          continue;
        }
        if (set[i].semver.prerelease.length > 0) {
          const allowed = set[i].semver;
          if (allowed.major === version2.major && allowed.minor === version2.minor && allowed.patch === version2.patch) {
            return true;
          }
        }
      }
      return false;
    }
    return true;
  };
  return range;
}
var satisfies_1;
var hasRequiredSatisfies;
function requireSatisfies() {
  if (hasRequiredSatisfies) return satisfies_1;
  hasRequiredSatisfies = 1;
  const Range = requireRange();
  const satisfies2 = (version2, range2, options) => {
    try {
      range2 = new Range(range2, options);
    } catch (er) {
      return false;
    }
    return range2.test(version2);
  };
  satisfies_1 = satisfies2;
  return satisfies_1;
}
var satisfiesExports = requireSatisfies();
var satisfies = /* @__PURE__ */ getDefaultExportFromCjs(satisfiesExports);
function post(window2, url, data) {
  const editor = window2.open(url);
  const step = 250;
  const { origin } = new URL(url);
  let count = 40;
  function listen(evt) {
    if (evt.source === editor) {
      count = 0;
      window2.removeEventListener("message", listen, false);
    }
  }
  window2.addEventListener("message", listen, false);
  function send() {
    if (count <= 0) {
      return;
    }
    editor.postMessage(data, origin);
    setTimeout(send, step);
    count -= 1;
  }
  setTimeout(send, step);
}
var embedStyle = `.vega-embed {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
}
.vega-embed.has-actions {
  padding-right: 38px;
}
.vega-embed details:not([open]) > :not(summary) {
  display: none !important;
}
.vega-embed summary {
  list-style: none;
  position: absolute;
  top: 0;
  right: 0;
  padding: 6px;
  z-index: 1000;
  background: white;
  box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
  color: #1b1e23;
  border: 1px solid #aaa;
  border-radius: 999px;
  opacity: 0.2;
  transition: opacity 0.4s ease-in;
  cursor: pointer;
  line-height: 0px;
}
.vega-embed summary::-webkit-details-marker {
  display: none;
}
.vega-embed summary:active {
  box-shadow: #aaa 0px 0px 0px 1px inset;
}
.vega-embed summary svg {
  width: 14px;
  height: 14px;
}
.vega-embed details[open] summary {
  opacity: 0.7;
}
.vega-embed:hover summary, .vega-embed:focus-within summary {
  opacity: 1 !important;
  transition: opacity 0.2s ease;
}
.vega-embed .vega-actions {
  position: absolute;
  z-index: 1001;
  top: 35px;
  right: -9px;
  display: flex;
  flex-direction: column;
  padding-bottom: 8px;
  padding-top: 8px;
  border-radius: 4px;
  box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
  border: 1px solid #d9d9d9;
  background: white;
  animation-duration: 0.15s;
  animation-name: scale-in;
  animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5);
  text-align: left;
}
.vega-embed .vega-actions a {
  padding: 8px 16px;
  font-family: sans-serif;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  color: #434a56;
  text-decoration: none;
}
.vega-embed .vega-actions a:hover, .vega-embed .vega-actions a:focus {
  background-color: #f7f7f9;
  color: black;
}
.vega-embed .vega-actions::before, .vega-embed .vega-actions::after {
  content: "";
  display: inline-block;
  position: absolute;
}
.vega-embed .vega-actions::before {
  left: auto;
  right: 14px;
  top: -16px;
  border: 8px solid rgba(0, 0, 0, 0);
  border-bottom-color: #d9d9d9;
}
.vega-embed .vega-actions::after {
  left: auto;
  right: 15px;
  top: -14px;
  border: 7px solid rgba(0, 0, 0, 0);
  border-bottom-color: #fff;
}
.vega-embed .chart-wrapper.fit-x {
  width: 100%;
}
.vega-embed .chart-wrapper.fit-y {
  height: 100%;
}

.vega-embed-wrapper {
  max-width: 100%;
  overflow: auto;
  padding-right: 14px;
}

@keyframes scale-in {
  from {
    opacity: 0;
    transform: scale(0.6);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}
`;
function mergeDeep(dest, ...src) {
  for (const s of src) {
    deepMerge_(dest, s);
  }
  return dest;
}
function deepMerge_(dest, src) {
  for (const property of Object.keys(src)) {
    writeConfig(dest, property, src[property], true);
  }
}
const vega = vegaImport;
let vegaLite = vegaLiteImport;
const w = typeof window !== "undefined" ? window : void 0;
if (vegaLite === void 0 && w?.vl?.compile) {
  vegaLite = w.vl;
}
const DEFAULT_ACTIONS = { export: { svg: true, png: true }, source: true, compiled: true, editor: true };
const I18N = {
  CLICK_TO_VIEW_ACTIONS: "Click to view actions",
  COMPILED_ACTION: "View Compiled Vega",
  EDITOR_ACTION: "Open in Vega Editor",
  PNG_ACTION: "Save as PNG",
  SOURCE_ACTION: "View Source",
  SVG_ACTION: "Save as SVG"
};
const NAMES = {
  vega: "Vega",
  "vega-lite": "Vega-Lite"
};
const VERSION = {
  vega: vega.version,
  "vega-lite": vegaLite ? vegaLite.version : "not available"
};
const PREPROCESSOR = {
  vega: (vgSpec) => vgSpec,
  "vega-lite": (vlSpec, logger2, config) => vegaLite.compile(vlSpec, { config, logger: logger2 }).spec
};
const SVG_CIRCLES = `
<svg viewBox="0 0 16 16" fill="currentColor" stroke="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
  <circle r="2" cy="8" cx="2"></circle>
  <circle r="2" cy="8" cx="8"></circle>
  <circle r="2" cy="8" cx="14"></circle>
</svg>`;
const CHART_WRAPPER_CLASS = "chart-wrapper";
function isTooltipHandler(h) {
  return typeof h === "function";
}
function viewSource(source, sourceHeader, sourceFooter, mode) {
  const header = `<html><head>${sourceHeader}</head><body><pre><code class="json">`;
  const footer = `</code></pre>${sourceFooter}</body></html>`;
  const win = window.open("");
  win.document.write(header + source + footer);
  win.document.title = `${NAMES[mode]} JSON Source`;
}
function guessMode(spec, logger2, providedMode) {
  if (spec.$schema) {
    const parsed = e(spec.$schema);
    if (providedMode && providedMode !== parsed.library) {
      logger2.warn(`The given visualization spec is written in ${NAMES[parsed.library]}, but mode argument sets ${NAMES[providedMode] ?? providedMode}.`);
    }
    const mode = parsed.library;
    if (!satisfies(VERSION[mode], `^${parsed.version.slice(1)}`)) {
      logger2.warn(`The input spec uses ${NAMES[mode]} ${parsed.version}, but the current version of ${NAMES[mode]} is v${VERSION[mode]}.`);
    }
    return mode;
  }
  if ("mark" in spec || "encoding" in spec || "layer" in spec || "hconcat" in spec || "vconcat" in spec || "facet" in spec || "repeat" in spec) {
    return "vega-lite";
  }
  if ("marks" in spec || "signals" in spec || "scales" in spec || "axes" in spec) {
    return "vega";
  }
  return providedMode ?? "vega";
}
function isLoader(o) {
  return !!(o && "load" in o);
}
function createLoader(opts) {
  return isLoader(opts) ? opts : vega.loader(opts);
}
function embedOptionsFromUsermeta(parsedSpec) {
  const opts = parsedSpec.usermeta?.embedOptions ?? {};
  if (isString(opts.defaultStyle)) {
    opts.defaultStyle = false;
  }
  return opts;
}
async function embed(el, spec, opts = {}) {
  let parsedSpec;
  let loader;
  if (isString(spec)) {
    loader = createLoader(opts.loader);
    parsedSpec = JSON.parse(await loader.load(spec));
  } else {
    parsedSpec = spec;
  }
  const loadedEmbedOptions = embedOptionsFromUsermeta(parsedSpec);
  const usermetaLoader = loadedEmbedOptions.loader;
  if (!loader || usermetaLoader) {
    loader = createLoader(opts.loader ?? usermetaLoader);
  }
  const usermetaOpts = await loadOpts(loadedEmbedOptions, loader);
  const parsedOpts = await loadOpts(opts, loader);
  const mergedOpts = {
    ...mergeDeep(parsedOpts, usermetaOpts),
    config: mergeConfig(parsedOpts.config ?? {}, usermetaOpts.config ?? {})
  };
  return await _embed(el, parsedSpec, mergedOpts, loader);
}
async function loadOpts(opt, loader) {
  const config = isString(opt.config) ? JSON.parse(await loader.load(opt.config)) : opt.config ?? {};
  const patch = isString(opt.patch) ? JSON.parse(await loader.load(opt.patch)) : opt.patch;
  return {
    ...opt,
    ...patch ? { patch } : {},
    ...config ? { config } : {}
  };
}
function getRoot(el) {
  const possibleRoot = el.getRootNode ? el.getRootNode() : document;
  return possibleRoot instanceof ShadowRoot ? { root: possibleRoot, rootContainer: possibleRoot } : { root: document, rootContainer: document.head ?? document.body };
}
async function _embed(el, spec, opts = {}, loader) {
  const config = opts.theme ? mergeConfig(themes[opts.theme], opts.config ?? {}) : opts.config;
  const actions = isBoolean$2(opts.actions) ? opts.actions : mergeDeep({}, DEFAULT_ACTIONS, opts.actions ?? {});
  const i18n = { ...I18N, ...opts.i18n };
  const renderer = opts.renderer ?? "svg";
  const logger$1 = opts.logger ?? logger(vega.Warn);
  if (opts.logLevel !== void 0)
    logger$1.level(opts.logLevel);
  const downloadFileName = opts.downloadFileName ?? "visualization";
  const element = typeof el === "string" ? document.querySelector(el) : el;
  if (!element) {
    throw new Error(`${el} does not exist`);
  }
  if (opts.defaultStyle !== false) {
    const ID = "vega-embed-style";
    const { root, rootContainer } = getRoot(element);
    if (!root.getElementById(ID)) {
      const style = document.createElement("style");
      style.id = ID;
      style.innerHTML = opts.defaultStyle === void 0 || opts.defaultStyle === true ? embedStyle.toString() : opts.defaultStyle;
      rootContainer.appendChild(style);
    }
  }
  const mode = guessMode(spec, logger$1, opts.mode);
  let vgSpec = PREPROCESSOR[mode](spec, logger$1, config);
  if (mode === "vega-lite") {
    if (vgSpec.$schema) {
      const parsed = e(vgSpec.$schema);
      if (!satisfies(VERSION.vega, `^${parsed.version.slice(1)}`)) {
        logger$1.warn(`The compiled spec uses Vega ${parsed.version}, but current version is v${VERSION.vega}.`);
      }
    }
  }
  element.classList.add("vega-embed");
  if (actions) {
    element.classList.add("has-actions");
  }
  element.innerHTML = "";
  let container = element;
  if (actions) {
    const chartWrapper = document.createElement("div");
    chartWrapper.classList.add(CHART_WRAPPER_CLASS);
    element.appendChild(chartWrapper);
    container = chartWrapper;
  }
  const patch = opts.patch;
  if (patch) {
    vgSpec = patch instanceof Function ? patch(vgSpec) : applyPatch(vgSpec, patch, true, false).newDocument;
  }
  if (opts.formatLocale) {
    vega.formatLocale(opts.formatLocale);
  }
  if (opts.timeFormatLocale) {
    vega.timeFormatLocale(opts.timeFormatLocale);
  }
  if (opts.expressionFunctions) {
    for (const name in opts.expressionFunctions) {
      const expressionFunction = opts.expressionFunctions[name];
      if ("fn" in expressionFunction) {
        vega.expressionFunction(name, expressionFunction.fn, expressionFunction["visitor"]);
      } else if (expressionFunction instanceof Function) {
        vega.expressionFunction(name, expressionFunction);
      }
    }
  }
  const { ast } = opts;
  const runtime = vega.parse(vgSpec, mode === "vega-lite" ? {} : config, { ast });
  const view = new (opts.viewClass || vega.View)(runtime, {
    loader,
    logger: logger$1,
    renderer,
    ...ast ? { expr: vega.expressionInterpreter ?? opts.expr ?? expression$1 } : {}
  });
  view.addSignalListener("autosize", (_, autosize) => {
    const { type } = autosize;
    if (type == "fit-x") {
      container.classList.add("fit-x");
      container.classList.remove("fit-y");
    } else if (type == "fit-y") {
      container.classList.remove("fit-x");
      container.classList.add("fit-y");
    } else if (type == "fit") {
      container.classList.add("fit-x", "fit-y");
    } else {
      container.classList.remove("fit-x", "fit-y");
    }
  });
  if (opts.tooltip !== false) {
    const { loader: loader_, tooltip } = opts;
    const baseURL = loader_ && !isLoader(loader_) ? loader_?.baseURL : void 0;
    const handler = isTooltipHandler(tooltip) ? tooltip : (
      // user provided boolean true or tooltip options
      new Handler({ baseURL, ...tooltip === true ? {} : tooltip }).call
    );
    view.tooltip(handler);
  }
  let { hover } = opts;
  if (hover === void 0) {
    hover = mode === "vega";
  }
  if (hover) {
    const { hoverSet, updateSet } = typeof hover === "boolean" ? {} : hover;
    view.hover(hoverSet, updateSet);
  }
  if (opts) {
    if (opts.width != null) {
      view.width(opts.width);
    }
    if (opts.height != null) {
      view.height(opts.height);
    }
    if (opts.padding != null) {
      view.padding(opts.padding);
    }
  }
  await view.initialize(container, opts.bind).runAsync();
  let documentClickHandler;
  if (actions !== false) {
    let wrapper = element;
    if (opts.defaultStyle !== false || opts.forceActionsMenu) {
      const details = document.createElement("details");
      details.title = i18n.CLICK_TO_VIEW_ACTIONS;
      element.append(details);
      wrapper = details;
      const summary = document.createElement("summary");
      summary.innerHTML = SVG_CIRCLES;
      details.append(summary);
      documentClickHandler = (ev) => {
        if (!details.contains(ev.target)) {
          details.removeAttribute("open");
        }
      };
      document.addEventListener("click", documentClickHandler);
    }
    const ctrl = document.createElement("div");
    wrapper.append(ctrl);
    ctrl.classList.add("vega-actions");
    if (actions === true || actions.export !== false) {
      for (const ext of ["svg", "png"]) {
        if (actions === true || actions.export === true || actions.export[ext]) {
          const i18nExportAction = i18n[`${ext.toUpperCase()}_ACTION`];
          const exportLink = document.createElement("a");
          const scaleFactor = isObject(opts.scaleFactor) ? opts.scaleFactor[ext] : opts.scaleFactor;
          exportLink.text = i18nExportAction;
          exportLink.href = "#";
          exportLink.target = "_blank";
          exportLink.download = `${downloadFileName}.${ext}`;
          exportLink.addEventListener("mousedown", async function(e) {
            e.preventDefault();
            const url = await view.toImageURL(ext, scaleFactor);
            this.href = url;
          });
          ctrl.append(exportLink);
        }
      }
    }
    if (actions === true || actions.source !== false) {
      const viewSourceLink = document.createElement("a");
      viewSourceLink.text = i18n.SOURCE_ACTION;
      viewSourceLink.href = "#";
      viewSourceLink.addEventListener("click", function(e) {
        viewSource(stringify$2(spec), opts.sourceHeader ?? "", opts.sourceFooter ?? "", mode);
        e.preventDefault();
      });
      ctrl.append(viewSourceLink);
    }
    if (mode === "vega-lite" && (actions === true || actions.compiled !== false)) {
      const compileLink = document.createElement("a");
      compileLink.text = i18n.COMPILED_ACTION;
      compileLink.href = "#";
      compileLink.addEventListener("click", function(e) {
        viewSource(stringify$2(vgSpec), opts.sourceHeader ?? "", opts.sourceFooter ?? "", "vega");
        e.preventDefault();
      });
      ctrl.append(compileLink);
    }
    if (actions === true || actions.editor !== false) {
      const editorUrl = opts.editorUrl ?? "https://vega.github.io/editor/";
      const editorLink = document.createElement("a");
      editorLink.text = i18n.EDITOR_ACTION;
      editorLink.href = "#";
      editorLink.addEventListener("click", function(e) {
        post(window, editorUrl, {
          config,
          mode: patch ? "vega" : mode,
          renderer,
          spec: stringify$2(patch ? vgSpec : spec)
        });
        e.preventDefault();
      });
      ctrl.append(editorLink);
    }
  }
  function finalize() {
    if (documentClickHandler) {
      document.removeEventListener("click", documentClickHandler);
    }
    view.finalize();
  }
  return { view, spec, vgSpec, finalize, embedOptions: opts };
}

export { DEFAULT_ACTIONS, embed as default, guessMode, vega, vegaLite };
//# sourceMappingURL=embed-t6zjks9U.js.map
