files: import warnings
  
  import hashlib
  import io
  import json
  import jsonschema
  import pandas as pd
  from toolz.curried import pipe as _pipe
  
  from .schema import core, channels, mixins, Undefined, SCHEMA_URL
  
  from .data import data_transformers
  from ... import utils, expr
  from .display import renderers, VEGALITE_VERSION, VEGAEMBED_VERSION, VEGA_VERSION
  from .theme import themes
  
  
  # ------------------------------------------------------------------------
  # Data Utilities
  def _dataset_name(values):
      """Generate a unique hash of the data
  
      Parameters
      ----------
      values : list or dict
          A list/dict representation of data values.
  
      Returns
      -------
      name : string
          A unique name generated from the hash of the values.
      """
      if isinstance(values, core.InlineDataset):
          values = values.to_dict()
      if values == [{}]:
          return "empty"
      values_json = json.dumps(values, sort_keys=True)
      hsh = hashlib.md5(values_json.encode()).hexdigest()
      return "data-" + hsh
  
  
  def _consolidate_data(data, context):
      """If data is specified inline, then move it to context['datasets']
  
      This function will modify context in-place, and return a new version of data
      """
      values = Undefined
      kwds = {}
  
      if isinstance(data, core.InlineData):
          if data.name is Undefined and data.values is not Undefined:
              if isinstance(data.values, core.InlineDataset):
                  values = data.to_dict()["values"]
              else:
                  values = data.values
              kwds = {"format": data.format}
  
      elif isinstance(data, dict):
          if "name" not in data and "values" in data:
              values = data["values"]
              kwds = {k: v for k, v in data.items() if k != "values"}
  
      if values is not Undefined:
          name = _dataset_name(values)
          data = core.NamedData(name=name, **kwds)
          context.setdefault("datasets", {})[name] = values
  
      return data
  
  
  def _prepare_data(data, context=None):
      """Convert input data to data for use within schema
  
      Parameters
      ----------
      data :
          The input dataset in the form of a DataFrame, dictionary, altair data
          object, or other type that is recognized by the data transformers.
      context : dict (optional)
          The to_dict context in which the data is being prepared. This is used
          to keep track of information that needs to be passed up and down the
          recursive serialization routine, such as global named datasets.
      """
      if data is Undefined:
          return data
  
      # convert dataframes  or objects with __geo_interface__ to dict
      if isinstance(data, pd.DataFrame) or hasattr(data, "__geo_interface__"):
          data = _pipe(data, data_transformers.get())
  
      # convert string input to a URLData
      if isinstance(data, str):
          data = core.UrlData(data)
  
      # consolidate inline data to top-level datasets
      if context is not None and data_transformers.consolidate_datasets:
          data = _consolidate_data(data, context)
  
      # if data is still not a recognized type, then return
      if not isinstance(data, (dict, core.Data)):
          warnings.warn("data of type {} not recognized".format(type(data)))
  
      return data
  
  
  # ------------------------------------------------------------------------
  # Aliases & specializations
  Bin = core.BinParams
  
  
  @utils.use_signature(core.LookupData)
  class LookupData(core.LookupData):
      def to_dict(self, *args, **kwargs):
          """Convert the chart to a dictionary suitable for JSON export."""
          copy = self.copy(deep=False)
          copy.data = _prepare_data(copy.data, kwargs.get("context"))
          return super(LookupData, copy).to_dict(*args, **kwargs)
  
  
  @utils.use_signature(core.FacetMapping)
  class FacetMapping(core.FacetMapping):
      _class_is_valid_at_instantiation = False
  
      def to_dict(self, *args, **kwargs):
          copy = self.copy(deep=False)
          context = kwargs.get("context", {})
          data = context.get("data", None)
          if isinstance(self.row, str):
              copy.row = core.FacetFieldDef(**utils.parse_shorthand(self.row, data))
          if isinstance(self.column, str):
              copy.column = core.FacetFieldDef(**utils.parse_shorthand(self.column, data))
          return super(FacetMapping, copy).to_dict(*args, **kwargs)
  
  
  # ------------------------------------------------------------------------
  # Encoding will contain channel objects that aren't valid at instantiation
  core.FacetedEncoding._class_is_valid_at_instantiation = False
  
  # ------------------------------------------------------------------------
  # These are parameters that are valid at the top level, but are not valid
  # for specs that are within a composite chart
  # (layer, hconcat, vconcat, facet, repeat)
  TOPLEVEL_ONLY_KEYS = {"background", "config", "autosize", "padding", "$schema"}
  
  
  def _get_channels_mapping():
      mapping = {}
      for attr in dir(channels):
          cls = getattr(channels, attr)
          if isinstance(cls, type) and issubclass(cls, core.SchemaBase):
              mapping[cls] = attr.replace("Value", "").lower()
      return mapping
  
  
  # -------------------------------------------------------------------------
  # Tools for working with parameters
  class Parameter(expr.core.OperatorMixin, object):
      """A Parameter object"""
  
      _counter = 0
  
      @classmethod
      def _get_name(cls):
          cls._counter += 1
          return "parameter{:03d}".format(cls._counter)
  
      def __init__(self, name):
          if name is None:
              name = self._get_name()
          self.name = name
  
      def to_dict(self):
          if self.param_type == "variable":
              if self.param.expr is Undefined:
                  return {"expr": self.name}
              else:
                  return {"expr": repr(self.param.expr)}
          elif self.param_type == "selection":
              return {
                  "param": self.name.to_dict()
                  if hasattr(self.name, "to_dict")
                  else self.name
              }
  
      def __invert__(self):
          if self.param_type == "selection":
              return SelectionPredicateComposition({"not": {"param": self.name}})
          else:
              return expr.core.OperatorMixin.__invert__(self)
  
      def __and__(self, other):
          if self.param_type == "selection":
              if isinstance(other, Parameter):
                  other = {"param": other.name}
              return SelectionPredicateComposition({"and": [{"param": self.name}, other]})
          else:
              return expr.core.OperatorMixin.__and__(self, other)
  
      def __or__(self, other):
          if self.param_type == "selection":
              if isinstance(other, Parameter):
                  other = {"param": other.name}
              return SelectionPredicateComposition({"or": [{"param": self.name}, other]})
          else:
              return expr.core.OperatorMixin.__or__(self, other)
  
      def __repr__(self):
          return "Parameter({0!r}, {1})".format(self.name, self.param)
  
      def _to_expr(self):
          if not hasattr(self.param, "expr") or self.param.expr is Undefined:
              return self.name
          else:
              return self.param.expr
  
      def _from_expr(self, expr):
          return parameter(expr=expr)
  
      def __getattr__(self, field_name):
          if field_name.startswith("__") and field_name.endswith("__"):
              raise AttributeError(field_name)
          _attrexpr = expr.core.GetAttrExpression(self.name, field_name)
          # If self is a SelectionParameter and field_name is in its
          # fields or encodings list, then we want to return an expression.
          if check_fields_and_encodings(self, field_name):
              return SelectionExpression(_attrexpr)
          return expr.core.GetAttrExpression(self.name, field_name)
  
  
  # Enables use of ~, &, | with compositions of selection objects.
  class SelectionPredicateComposition(core.PredicateComposition):
      def __invert__(self):
          return SelectionPredicateComposition({"not": self.to_dict()})
  
      def __and__(self, other):
          return SelectionPredicateComposition({"and": [self.to_dict(), other.to_dict()]})
  
      def __or__(self, other):
          return SelectionPredicateComposition({"or": [self.to_dict(), other.to_dict()]})
  
  
  class SelectionExpression(expr.core.OperatorMixin, object):
      def __init__(self, expr):
          self.expr = expr
  
      def to_dict(self):
          return {"expr": repr(self.expr)}
  
      def _to_expr(self):
          return repr(self.expr)
  
      def _from_expr(self, expr):
          return SelectionExpression(expr=expr)
  
  
  def check_fields_and_encodings(parameter, field_name):
      for prop in ["fields", "encodings"]:
          try:
              if field_name in getattr(parameter.param.select, prop):
                  return True
          except (AttributeError, TypeError):
              pass
  
      return False
  
  
  # ------------------------------------------------------------------------
  # Top-Level Functions
  
  
  def value(value, **kwargs):
      """Specify a value for use in an encoding"""
      return dict(value=value, **kwargs)
  
  
  def parameter(name=None, select=None, **kwds):
      """Create a named parameter.
  
      Parameters
      ----------
      name : string (optional)
          The name of the parameter. If not specified, a unique name will be
          created.
      **kwds :
          additional keywords will be used to construct a parameter.  If 'select'
          is among the keywords, then a SelectionParameter will be created.
          Otherwise, a VariableParameter will be created.
  
      Returns
      -------
      parameter: Parameter
          The parameter object that can be used in chart creation.
      """
  
      parameter = Parameter(name)
  
      if "empty" in kwds:
          parameter.empty = kwds.pop("empty")
          if parameter.empty == "none":
              warnings.warn(
                  """The value of 'empty' should be True or False.""",
                  DeprecationWarning,
              )
              parameter.empty = False
          elif parameter.empty == "all":
              warnings.warn(
                  """The value of 'empty' should be True or False.""",
                  DeprecationWarning,
              )
              parameter.empty = True
          elif (parameter.empty is False) or (parameter.empty is True):
              pass
          else:
              raise ValueError("The value of 'empty' should be True or False.")
  
      if "init" in kwds:
          warnings.warn(
              """Use 'value' instead of 'init'.""",
              DeprecationWarning,
          )
          if "value" not in kwds:
              kwds["value"] = kwds.pop("init")
          else:
              # If both 'value' and 'init' are set, we ignore 'init'.
              kwds.pop("init")
  
      if select is None:
          parameter.param = core.VariableParameter(name=parameter.name, **kwds)
          parameter.param_type = "variable"
      else:
          parameter.param = core.SelectionParameter(
              name=parameter.name, select=select, **kwds
          )
          parameter.param_type = "selection"
  
      return parameter
  
  
  # TODO: Update the docstring
  def selection(type=Undefined, **kwds):
      """Create a selection.
  
      Parameters
      ----------
      type : string
          The type of the selection: either "interval" or "point"]
      **kwds :
          additional keywords to control the selection.
  
      Returns
      -------
      Parameter
          The Parameter object that can be used in chart creation.
      """
  
      # We separate out the parameter keywords from the selection keywords
      param_kwds = {}
  
      for kwd in {"name", "value", "bind", "empty", "init"}:
          if kwd in kwds:
              param_kwds[kwd] = kwds.pop(kwd)
  
      if type == "interval":
          select = core.IntervalSelectionConfig(type=type, **kwds)
      elif type == "point":
          select = core.PointSelectionConfig(type=type, **kwds)
      elif type in ["single", "multi"]:
          select = core.PointSelectionConfig(type="point", **kwds)
          warnings.warn(
              """The types 'single' and 'multi' are now
          combined and should be specified using "type='point'".""",
              DeprecationWarning,
          )
      else:
          raise ValueError("""'type' must be 'point' or 'interval'""")
  
      return parameter(select=select, **param_kwds)
  
  
  @utils.use_signature(core.IntervalSelectionConfig)
  def selection_interval(**kwargs):
      """Create a selection parameter with type='interval'"""
      return selection(type="interval", **kwargs)
  
  
  @utils.use_signature(core.PointSelectionConfig)
  def selection_point(**kwargs):
      """Create a selection with type='point'"""
      return selection(type="point", **kwargs)
  
  
  @utils.use_signature(core.PointSelectionConfig)
  def selection_multi(**kwargs):
      warnings.warn(
          """'selection_multi' is deprecated.  Use 'selection_point'""",
          DeprecationWarning,
      )
      return selection(type="point", **kwargs)
  
  
  @utils.use_signature(core.PointSelectionConfig)
  def selection_single(**kwargs):
      warnings.warn(
          """'selection_single' is deprecated.  Use 'selection_point'""",
          DeprecationWarning,
      )
      return selection(type="point", **kwargs)
  
  
  @utils.use_signature(core.Binding)
  def binding(input, **kwargs):
      """A generic binding"""
      return core.Binding(input=input, **kwargs)
  
  
  @utils.use_signature(core.BindCheckbox)
  def binding_checkbox(**kwargs):
      """A checkbox binding"""
      return core.BindCheckbox(input="checkbox", **kwargs)
  
  
  @utils.use_signature(core.BindRadioSelect)
  def binding_radio(**kwargs):
      """A radio button binding"""
      return core.BindRadioSelect(input="radio", **kwargs)
  
  
  @utils.use_signature(core.BindRadioSelect)
  def binding_select(**kwargs):
      """A select binding"""
      return core.BindRadioSelect(input="select", **kwargs)
  
  
  @utils.use_signature(core.BindRange)
  def binding_range(**kwargs):
      """A range binding"""
      return core.BindRange(input="range", **kwargs)
  
  
  # TODO: update the docstring
  def condition(predicate, if_true, if_false, **kwargs):
      """A conditional attribute or encoding
  
      Parameters
      ----------
      predicate: Selection, PredicateComposition, expr.Expression, dict, or string
          the selection predicate or test predicate for the condition.
          if a string is passed, it will be treated as a test operand.
      if_true:
          the spec or object to use if the selection predicate is true
      if_false:
          the spec or object to use if the selection predicate is false
      **kwargs:
          additional keyword args are added to the resulting dict
  
      Returns
      -------
      spec: dict or VegaLiteSchema
          the spec that describes the condition
      """
      test_predicates = (str, expr.Expression, core.PredicateComposition)
  
      if isinstance(predicate, Parameter):
          if predicate.param_type == "selection" or predicate.param.expr is Undefined:
              condition = {"param": predicate.name}
              if "empty" in kwargs:
                  condition["empty"] = kwargs.pop("empty")
              elif isinstance(predicate.empty, bool):
                  condition["empty"] = predicate.empty
          else:
              condition = {"test": predicate.param.expr}
      elif isinstance(predicate, test_predicates):
          condition = {"test": predicate}
      elif isinstance(predicate, dict):
          condition = predicate
      else:
          raise NotImplementedError(
              "condition predicate of type {}" "".format(type(predicate))
          )
  
      if isinstance(if_true, core.SchemaBase):
          # convert to dict for now; the from_dict call below will wrap this
          # dict in the appropriate schema
          if_true = if_true.to_dict()
      elif isinstance(if_true, str):
          if_true = {"shorthand": if_true}
          if_true.update(kwargs)
      condition.update(if_true)
  
      if isinstance(if_false, core.SchemaBase):
          # For the selection, the channel definitions all allow selections
          # already. So use this SchemaBase wrapper if possible.
          selection = if_false.copy()
          selection.condition = condition
      elif isinstance(if_false, str):
          selection = {"condition": condition, "shorthand": if_false}
          selection.update(kwargs)
      else:
          selection = dict(condition=condition, **if_false)
  
      return selection
  
  
  # --------------------------------------------------------------------
  # Top-level objects
  
  
  class TopLevelMixin(mixins.ConfigMethodMixin):
      """Mixin for top-level chart objects such as Chart, LayeredChart, etc."""
  
      _class_is_valid_at_instantiation = False
  
      def to_dict(self, *args, **kwargs):
          """Convert the chart to a dictionary suitable for JSON export"""
          # We make use of three context markers:
          # - 'data' points to the data that should be referenced for column type
          #   inference.
          # - 'top_level' is a boolean flag that is assumed to be true; if it's
          #   true then a "$schema" arg is added to the dict.
          # - 'datasets' is a dict of named datasets that should be inserted
          #   in the top-level object
  
          # note: not a deep copy because we want datasets and data arguments to
          # be passed by reference
          context = kwargs.get("context", {}).copy()
          context.setdefault("datasets", {})
          is_top_level = context.get("top_level", True)
  
          copy = self.copy(deep=False)
          original_data = getattr(copy, "data", Undefined)
          copy.data = _prepare_data(original_data, context)
  
          if original_data is not Undefined:
              context["data"] = original_data
  
          # remaining to_dict calls are not at top level
          context["top_level"] = False
          kwargs["context"] = context
  
          try:
              dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
          except jsonschema.ValidationError:
              dct = None
  
          # If we hit an error, then re-convert with validate='deep' to get
          # a more useful traceback. We don't do this by default because it's
          # much slower in the case that there are no errors.
          if dct is None:
              kwargs["validate"] = "deep"
              dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
  
          # TODO: following entries are added after validation. Should they be validated?
          if is_top_level:
              # since this is top-level we add $schema if it's missing
              if "$schema" not in dct:
                  dct["$schema"] = SCHEMA_URL
  
              # apply theme from theme registry
              the_theme = themes.get()
              dct = utils.update_nested(the_theme(), dct, copy=True)
  
              # update datasets
              if context["datasets"]:
                  dct.setdefault("datasets", {}).update(context["datasets"])
  
          return dct
  
      def to_html(
          self,
          base_url="https://cdn.jsdelivr.net/npm/",
          output_div="vis",
          embed_options=None,
          json_kwds=None,
          fullhtml=True,
          requirejs=False,
      ):
          return utils.spec_to_html(
              self.to_dict(),
              mode="vega-lite",
              vegalite_version=VEGALITE_VERSION,
              vegaembed_version=VEGAEMBED_VERSION,
              vega_version=VEGA_VERSION,
              base_url=base_url,
              output_div=output_div,
              embed_options=embed_options,
              json_kwds=json_kwds,
              fullhtml=fullhtml,
              requirejs=requirejs,
          )
  
      def save(
          self,
          fp,
          format=None,
          override_data_transformer=True,
          scale_factor=1.0,
          vegalite_version=VEGALITE_VERSION,
          vega_version=VEGA_VERSION,
          vegaembed_version=VEGAEMBED_VERSION,
          **kwargs,
      ):
          """Save a chart to file in a variety of formats
  
          Supported formats are json, html, png, svg, pdf; the last three require
          the altair_saver package to be installed.
  
          Parameters
          ----------
          fp : string filename or file-like object
              file in which to write the chart.
          format : string (optional)
              the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
              If not specified, the format will be determined from the filename.
          override_data_transformer : boolean (optional)
              If True (default), then the save action will be done with
              the MaxRowsError disabled. If False, then do not change the data
              transformer.
          scale_factor : float
              For svg or png formats, scale the image by this factor when saving.
              This can be used to control the size or resolution of the output.
              Default is 1.0
          **kwargs :
              Additional keyword arguments are passed to the output method
              associated with the specified format.
  
          """
          from ...utils.save import save
  
          kwds = dict(
              chart=self,
              fp=fp,
              format=format,
              scale_factor=scale_factor,
              vegalite_version=vegalite_version,
              vega_version=vega_version,
              vegaembed_version=vegaembed_version,
              **kwargs,
          )
  
          # By default we override the data transformer. This makes it so
          # that save() will succeed even for large datasets that would
          # normally trigger a MaxRowsError
          if override_data_transformer:
              with data_transformers.disable_max_rows():
                  result = save(**kwds)
          else:
              result = save(**kwds)
          return result
  
      # Fallback for when rendering fails; the full repr is too long to be
      # useful in nearly all cases.
      def __repr__(self):
          return "alt.{}(...)".format(self.__class__.__name__)
  
      # Layering and stacking
      def __add__(self, other):
          if not isinstance(other, TopLevelMixin):
              raise ValueError("Only Chart objects can be layered.")
          return layer(self, other)
  
      def __and__(self, other):
          if not isinstance(other, TopLevelMixin):
              raise ValueError("Only Chart objects can be concatenated.")
          return vconcat(self, other)
  
      def __or__(self, other):
          if not isinstance(other, TopLevelMixin):
              raise ValueError("Only Chart objects can be concatenated.")
          return hconcat(self, other)
  
      def repeat(
          self,
          repeat=Undefined,
          row=Undefined,
          column=Undefined,
          layer=Undefined,
          columns=Undefined,
          **kwargs,
      ):
          """Return a RepeatChart built from the chart
  
          Fields within the chart can be set to correspond to the row or
          column using `alt.repeat('row')` and `alt.repeat('column')`.
  
          Parameters
          ----------
          repeat : list
              a list of data column names to be repeated. This cannot be
              used along with the ``row``, ``column`` or ``layer`` argument.
          row : list
              a list of data column names to be mapped to the row facet
          column : list
              a list of data column names to be mapped to the column facet
          layer : list
              a list of data column names to be layered. This cannot be
              used along with the ``row``, ``column`` or ``repeat`` argument.
          columns : int
              the maximum number of columns before wrapping. Only referenced
              if ``repeat`` is specified.
          **kwargs :
              additional keywords passed to RepeatChart.
  
          Returns
          -------
          chart : RepeatChart
              a repeated chart.
          """
          repeat_specified = repeat is not Undefined
          rowcol_specified = row is not Undefined or column is not Undefined
          layer_specified = layer is not Undefined
  
          if repeat_specified and rowcol_specified:
              raise ValueError(
                  "repeat argument cannot be combined with row/column argument."
              )
          elif repeat_specified and layer_specified:
              raise ValueError("repeat argument cannot be combined with layer argument.")
          elif layer_specified and rowcol_specified:
              raise ValueError(
                  "layer argument cannot be combined with row/column argument."
              )
  
          if repeat_specified:
              repeat = repeat
          elif layer_specified:
              repeat = core.LayerRepeatMapping(layer=layer)
          else:
              repeat = core.RepeatMapping(row=row, column=column)
  
          return RepeatChart(spec=self, repeat=repeat, columns=columns, **kwargs)
  
      def properties(self, **kwargs):
          """Set top-level properties of the Chart.
  
          Argument names and types are the same as class initialization.
          """
          copy = self.copy(deep=False)
          for key, val in kwargs.items():
              if key == "selection" and isinstance(val, Parameter):
                  # TODO: Can this be removed
                  # For backward compatibility with old selection interface.
                  setattr(copy, key, {val.name: val.selection})
              else:
                  # Don't validate data, because it hasn't been processed.
                  if key != "data":
                      self.validate_property(key, val)
                  setattr(copy, key, val)
          return copy
  
      def project(
          self,
          type="mercator",
          center=Undefined,
          clipAngle=Undefined,
          clipExtent=Undefined,
          coefficient=Undefined,
          distance=Undefined,
          fraction=Undefined,
          lobes=Undefined,
          parallel=Undefined,
          precision=Undefined,
          radius=Undefined,
          ratio=Undefined,
          reflectX=Undefined,
          reflectY=Undefined,
          rotate=Undefined,
          scale=Undefined,
          spacing=Undefined,
          tilt=Undefined,
          translate=Undefined,
          **kwds,
      ):
          """Add a geographic projection to the chart.
  
          This is generally used either with ``mark_geoshape`` or with the
          ``latitude``/``longitude`` encodings.
  
          Available projection types are
          ['albers', 'albersUsa', 'azimuthalEqualArea', 'azimuthalEquidistant',
          'conicConformal', 'conicEqualArea', 'conicEquidistant', 'equalEarth', 'equirectangular',
          'gnomonic', 'identity', 'mercator', 'orthographic', 'stereographic', 'transverseMercator']
  
          Attributes
          ----------
          type : ProjectionType
              The cartographic projection to use. This value is case-insensitive, for example
              `"albers"` and `"Albers"` indicate the same projection type. You can find all valid
              projection types [in the
              documentation](https://vega.github.io/vega-lite/docs/projection.html#projection-types).
  
              **Default value:** `mercator`
          center : List(float)
              Sets the projection’s center to the specified center, a two-element array of
              longitude and latitude in degrees.
  
              **Default value:** `[0, 0]`
          clipAngle : float
              Sets the projection’s clipping circle radius to the specified angle in degrees. If
              `null`, switches to [antimeridian](http://bl.ocks.org/mbostock/3788999) cutting
              rather than small-circle clipping.
          clipExtent : List(List(float))
              Sets the projection’s viewport clip extent to the specified bounds in pixels. The
              extent bounds are specified as an array `[[x0, y0], [x1, y1]]`, where `x0` is the
              left-side of the viewport, `y0` is the top, `x1` is the right and `y1` is the
              bottom. If `null`, no viewport clipping is performed.
          coefficient : float
  
          distance : float
  
          fraction : float
  
          lobes : float
  
          parallel : float
  
          precision : Mapping(required=[length])
              Sets the threshold for the projection’s [adaptive
              resampling](http://bl.ocks.org/mbostock/3795544) to the specified value in pixels.
              This value corresponds to the [Douglas–Peucker
              distance](http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm).
               If precision is not specified, returns the projection’s current resampling
              precision which defaults to `√0.5 ≅ 0.70710…`.
          radius : float
  
          ratio : float
  
          reflectX : boolean
  
          reflectY : boolean
  
          rotate : List(float)
              Sets the projection’s three-axis rotation to the specified angles, which must be a
              two- or three-element array of numbers [`lambda`, `phi`, `gamma`] specifying the
              rotation angles in degrees about each spherical axis. (These correspond to yaw,
              pitch and roll.)
  
              **Default value:** `[0, 0, 0]`
          scale : float
              Sets the projection's scale (zoom) value, overriding automatic fitting.
  
          spacing : float
  
          tilt : float
  
          translate : List(float)
              Sets the projection's translation (pan) value, overriding automatic fitting.
  
          """
          projection = core.Projection(
              center=center,
              clipAngle=clipAngle,
              clipExtent=clipExtent,
              coefficient=coefficient,
              distance=distance,
              fraction=fraction,
              lobes=lobes,
              parallel=parallel,
              precision=precision,
              radius=radius,
              ratio=ratio,
              reflectX=reflectX,
              reflectY=reflectY,
              rotate=rotate,
              scale=scale,
              spacing=spacing,
              tilt=tilt,
              translate=translate,
              type=type,
              **kwds,
          )
          return self.properties(projection=projection)
  
      def _add_transform(self, *transforms):
          """Copy the chart and add specified transforms to chart.transform"""
          copy = self.copy(deep=["transform"])
          if copy.transform is Undefined:
              copy.transform = []
          copy.transform.extend(transforms)
          return copy
  
      def transform_aggregate(self, aggregate=Undefined, groupby=Undefined, **kwds):
          """
          Add an AggregateTransform to the schema.
  
          Parameters
          ----------
          aggregate : List(:class:`AggregatedFieldDef`)
              Array of objects that define fields to aggregate.
          groupby : List(string)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          **kwds :
              additional keywords are converted to aggregates using standard
              shorthand parsing.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          The aggregate transform allows you to specify transforms directly using
          the same shorthand syntax as used in encodings:
  
          >>> import altair as alt
          >>> chart1 = alt.Chart().transform_aggregate(
          ...     mean_acc='mean(Acceleration)',
          ...     groupby=['Origin']
          ... )
          >>> print(chart1.transform[0].to_json())  # doctest: +NORMALIZE_WHITESPACE
          {
            "aggregate": [
              {
                "as": "mean_acc",
                "field": "Acceleration",
                "op": "mean"
              }
            ],
            "groupby": [
              "Origin"
            ]
          }
  
          It also supports including AggregatedFieldDef instances or dicts directly,
          so you can create the above transform like this:
  
          >>> chart2 = alt.Chart().transform_aggregate(
          ...     [alt.AggregatedFieldDef(field='Acceleration', op='mean',
          ...                             **{'as': 'mean_acc'})],
          ...     groupby=['Origin']
          ... )
          >>> chart2.transform == chart1.transform
          True
  
          See Also
          --------
          alt.AggregateTransform : underlying transform object
  
          """
          if aggregate is Undefined:
              aggregate = []
          for key, val in kwds.items():
              parsed = utils.parse_shorthand(val)
              dct = {
                  "as": key,
                  "field": parsed.get("field", Undefined),
                  "op": parsed.get("aggregate", Undefined),
              }
              aggregate.append(core.AggregatedFieldDef(**dct))
          return self._add_transform(
              core.AggregateTransform(aggregate=aggregate, groupby=groupby)
          )
  
      def transform_bin(self, as_=Undefined, field=Undefined, bin=True, **kwargs):
          """
          Add a BinTransform to the schema.
  
          Parameters
          ----------
          as_ : anyOf(string, List(string))
              The output fields at which to write the start and end bin values.
          bin : anyOf(boolean, :class:`BinParams`)
              An object indicating bin properties, or simply ``true`` for using default bin
              parameters.
          field : string
              The data field to bin.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> chart = alt.Chart().transform_bin("x_binned", "x")
          >>> chart.transform[0]
          BinTransform({
            as: 'x_binned',
            bin: True,
            field: 'x'
          })
  
          >>> chart = alt.Chart().transform_bin("x_binned", "x",
          ...                                   bin=alt.Bin(maxbins=10))
          >>> chart.transform[0]
          BinTransform({
            as: 'x_binned',
            bin: BinParams({
              maxbins: 10
            }),
            field: 'x'
          })
  
          See Also
          --------
          alt.BinTransform : underlying transform object
  
          """
          if as_ is not Undefined:
              if "as" in kwargs:
                  raise ValueError(
                      "transform_bin: both 'as_' and 'as' passed as arguments."
                  )
              kwargs["as"] = as_
          kwargs["bin"] = bin
          kwargs["field"] = field
          return self._add_transform(core.BinTransform(**kwargs))
  
      def transform_calculate(self, as_=Undefined, calculate=Undefined, **kwargs):
          """
          Add a CalculateTransform to the schema.
  
          Parameters
          ----------
          as_ : string
              The field for storing the computed formula value.
          calculate : string or alt.expr expression
              A `expression <https://vega.github.io/vega-lite/docs/types.html#expression>`__
              string. Use the variable ``datum`` to refer to the current data object.
          **kwargs
              transforms can also be passed by keyword argument; see Examples
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> from altair import datum, expr
  
          >>> chart = alt.Chart().transform_calculate(y = 2 * expr.sin(datum.x))
          >>> chart.transform[0]
          CalculateTransform({
            as: 'y',
            calculate: (2 * sin(datum.x))
          })
  
          It's also possible to pass the ``CalculateTransform`` arguments directly:
  
          >>> kwds = {'as': 'y', 'calculate': '2 * sin(datum.x)'}
          >>> chart = alt.Chart().transform_calculate(**kwds)
          >>> chart.transform[0]
          CalculateTransform({
            as: 'y',
            calculate: '2 * sin(datum.x)'
          })
  
          As the first form is easier to write and understand, that is the
          recommended method.
  
          See Also
          --------
          alt.CalculateTransform : underlying transform object
          """
          if as_ is Undefined:
              as_ = kwargs.pop("as", Undefined)
          elif "as" in kwargs:
              raise ValueError(
                  "transform_calculate: both 'as_' and 'as' passed as arguments."
              )
          if as_ is not Undefined or calculate is not Undefined:
              dct = {"as": as_, "calculate": calculate}
              self = self._add_transform(core.CalculateTransform(**dct))
          for as_, calculate in kwargs.items():
              dct = {"as": as_, "calculate": calculate}
              self = self._add_transform(core.CalculateTransform(**dct))
          return self
  
      def transform_density(
          self,
          density,
          as_=Undefined,
          bandwidth=Undefined,
          counts=Undefined,
          cumulative=Undefined,
          extent=Undefined,
          groupby=Undefined,
          maxsteps=Undefined,
          minsteps=Undefined,
          steps=Undefined,
      ):
          """Add a DensityTransform to the spec.
  
          Attributes
          ----------
          density : str
              The data field for which to perform density estimation.
          as_ : [str, str]
              The output fields for the sample value and corresponding density estimate.
              **Default value:** ``["value", "density"]``
          bandwidth : float
              The bandwidth (standard deviation) of the Gaussian kernel. If unspecified or set to
              zero, the bandwidth value is automatically estimated from the input data using
              Scott’s rule.
          counts : boolean
              A boolean flag indicating if the output values should be probability estimates
              (false) or smoothed counts (true).
              **Default value:** ``false``
          cumulative : boolean
              A boolean flag indicating whether to produce density estimates (false) or cumulative
              density estimates (true).
              **Default value:** ``false``
          extent : List([float, float])
              A [min, max] domain from which to sample the distribution. If unspecified, the
              extent will be determined by the observed minimum and maximum values of the density
              value field.
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          maxsteps : float
              The maximum number of samples to take along the extent domain for plotting the
              density. **Default value:** ``200``
          minsteps : float
              The minimum number of samples to take along the extent domain for plotting the
              density. **Default value:** ``25``
          steps : float
              The exact number of samples to take along the extent domain for plotting the
              density. If specified, overrides both minsteps and maxsteps to set an exact number
              of uniform samples. Potentially useful in conjunction with a fixed extent to ensure
              consistent sample points for stacked densities.
          """
          return self._add_transform(
              core.DensityTransform(
                  density=density,
                  bandwidth=bandwidth,
                  counts=counts,
                  cumulative=cumulative,
                  extent=extent,
                  groupby=groupby,
                  maxsteps=maxsteps,
                  minsteps=minsteps,
                  steps=steps,
                  **{"as": as_},
              )
          )
  
      def transform_impute(
          self,
          impute,
          key,
          frame=Undefined,
          groupby=Undefined,
          keyvals=Undefined,
          method=Undefined,
          value=Undefined,
      ):
          """
          Add an ImputeTransform to the schema.
  
          Parameters
          ----------
          impute : string
              The data field for which the missing values should be imputed.
          key : string
              A key field that uniquely identifies data objects within a group.
              Missing key values (those occurring in the data but not in the current group) will
              be imputed.
          frame : List(anyOf(None, float))
              A frame specification as a two-element array used to control the window over which
              the specified method is applied. The array entries should either be a number
              indicating the offset from the current data object, or null to indicate unbounded
              rows preceding or following the current data object.  For example, the value ``[-5,
              5]`` indicates that the window should include five objects preceding and five
              objects following the current object.
              **Default value:** :  ``[null, null]`` indicating that the window includes all
              objects.
          groupby : List(string)
              An optional array of fields by which to group the values.
              Imputation will then be performed on a per-group basis.
          keyvals : anyOf(List(Mapping(required=[])), :class:`ImputeSequence`)
              Defines the key values that should be considered for imputation.
              An array of key values or an object defining a `number sequence
              <https://vega.github.io/vega-lite/docs/impute.html#sequence-def>`__.
              If provided, this will be used in addition to the key values observed within the
              input data.  If not provided, the values will be derived from all unique values of
              the ``key`` field. For ``impute`` in ``encoding``, the key field is the x-field if
              the y-field is imputed, or vice versa.
              If there is no impute grouping, this property *must* be specified.
          method : :class:`ImputeMethod`
              The imputation method to use for the field value of imputed data objects.
              One of ``value``, ``mean``, ``median``, ``max`` or ``min``.
              **Default value:**  ``"value"``
          value : Mapping(required=[])
              The field value to use when the imputation ``method`` is ``"value"``.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.ImputeTransform : underlying transform object
          """
          return self._add_transform(
              core.ImputeTransform(
                  impute=impute,
                  key=key,
                  frame=frame,
                  groupby=groupby,
                  keyvals=keyvals,
                  method=method,
                  value=value,
              )
          )
  
      def transform_joinaggregate(
          self, joinaggregate=Undefined, groupby=Undefined, **kwargs
      ):
          """
          Add a JoinAggregateTransform to the schema.
  
          Parameters
          ----------
          joinaggregate : List(:class:`JoinAggregateFieldDef`)
              The definition of the fields in the join aggregate, and what calculations to use.
          groupby : List(string)
              The data fields for partitioning the data objects into separate groups. If
              unspecified, all data points will be in a single group.
          **kwargs
              joinaggregates can also be passed by keyword argument; see Examples.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> chart = alt.Chart().transform_joinaggregate(x='sum(y)')
          >>> chart.transform[0]
          JoinAggregateTransform({
            joinaggregate: [JoinAggregateFieldDef({
              as: 'x',
              field: 'y',
              op: 'sum'
            })]
          })
  
          See Also
          --------
          alt.JoinAggregateTransform : underlying transform object
          """
          if joinaggregate is Undefined:
              joinaggregate = []
          for key, val in kwargs.items():
              parsed = utils.parse_shorthand(val)
              dct = {
                  "as": key,
                  "field": parsed.get("field", Undefined),
                  "op": parsed.get("aggregate", Undefined),
              }
              joinaggregate.append(core.JoinAggregateFieldDef(**dct))
          return self._add_transform(
              core.JoinAggregateTransform(joinaggregate=joinaggregate, groupby=groupby)
          )
  
      # TODO: Update docstring
      def transform_filter(self, filter, **kwargs):
          """
          Add a FilterTransform to the schema.
  
          Parameters
          ----------
          filter : a filter expression or :class:`PredicateComposition`
              The `filter` property must be one of the predicate definitions:
              (1) a string or alt.expr expression
              (2) a range predicate
              (3) a selection predicate
              (4) a logical operand combining (1)-(3)
              (5) a Selection object
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.FilterTransform : underlying transform object
  
          """
          if isinstance(filter, Parameter):
              new_filter = {"param": filter.name}
              if "empty" in kwargs:
                  new_filter["empty"] = kwargs.pop("empty")
              elif isinstance(filter.empty, bool):
                  new_filter["empty"] = filter.empty
              filter = new_filter
          return self._add_transform(core.FilterTransform(filter=filter, **kwargs))
  
      def transform_flatten(self, flatten, as_=Undefined):
          """Add a FlattenTransform to the schema.
  
          Parameters
          ----------
          flatten : List(string)
              An array of one or more data fields containing arrays to flatten.
              If multiple fields are specified, their array values should have a parallel
              structure, ideally with the same length.
              If the lengths of parallel arrays do not match,
              the longest array will be used with ``null`` values added for missing entries.
          as : List(string)
              The output field names for extracted array values.
              **Default value:** The field name of the corresponding array field
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.FlattenTransform : underlying transform object
          """
          return self._add_transform(
              core.FlattenTransform(flatten=flatten, **{"as": as_})
          )
  
      def transform_fold(self, fold, as_=Undefined):
          """Add a FoldTransform to the spec.
  
          Parameters
          ----------
          fold : List(string)
              An array of data fields indicating the properties to fold.
          as : [string, string]
              The output field names for the key and value properties produced by the fold
              transform. Default: ``["key", "value"]``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_pivot : pivot transform - opposite of fold.
          alt.FoldTransform : underlying transform object
          """
          return self._add_transform(core.FoldTransform(fold=fold, **{"as": as_}))
  
      def transform_loess(
          self, on, loess, as_=Undefined, bandwidth=Undefined, groupby=Undefined
      ):
          """Add a LoessTransform to the spec.
  
          Parameters
          ----------
          on : str
              The data field of the independent variable to use a predictor.
          loess : str
              The data field of the dependent variable to smooth.
          as_ : [str, str]
              The output field names for the smoothed points generated by the loess transform.
              **Default value:** The field names of the input x and y values.
          bandwidth : float
              A bandwidth parameter in the range ``[0, 1]`` that determines the amount of
              smoothing. **Default value:** ``0.3``
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_regression: regression transform
          alt.LoessTransform : underlying transform object
          """
          return self._add_transform(
              core.LoessTransform(
                  loess=loess, on=on, bandwidth=bandwidth, groupby=groupby, **{"as": as_}
              )
          )
  
      def transform_lookup(
          self,
          lookup=Undefined,
          from_=Undefined,
          as_=Undefined,
          default=Undefined,
          **kwargs,
      ):
          """Add a DataLookupTransform or SelectionLookupTransform to the chart
  
          Attributes
          ----------
          lookup : string
              Key in primary data source.
          from_ : anyOf(:class:`LookupData`, :class:`LookupSelection`)
              Secondary data reference.
          as_ : anyOf(string, List(string))
              The output fields on which to store the looked up data values.
  
              For data lookups, this property may be left blank if ``from_.fields``
              has been specified (those field names will be used); if ``from_.fields``
              has not been specified, ``as_`` must be a string.
  
              For selection lookups, this property is optional: if unspecified,
              looked up values will be stored under a property named for the selection;
              and if specified, it must correspond to ``from_.fields``.
          default : string
              The default value to use if lookup fails. **Default value:** ``null``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.DataLookupTransform : underlying transform object
          alt.SelectionLookupTransform : underlying transform object
          """
          if as_ is not Undefined:
              if "as" in kwargs:
                  raise ValueError(
                      "transform_lookup: both 'as_' and 'as' passed as arguments."
                  )
              kwargs["as"] = as_
          if from_ is not Undefined:
              if "from" in kwargs:
                  raise ValueError(
                      "transform_lookup: both 'from_' and 'from' passed as arguments."
                  )
              kwargs["from"] = from_
          kwargs["lookup"] = lookup
          kwargs["default"] = default
          return self._add_transform(core.LookupTransform(**kwargs))
  
      def transform_pivot(
          self, pivot, value, groupby=Undefined, limit=Undefined, op=Undefined
      ):
          """Add a pivot transform to the chart.
  
          Parameters
          ----------
          pivot : str
              The data field to pivot on. The unique values of this field become new field names
              in the output stream.
          value : str
              The data field to populate pivoted fields. The aggregate values of this field become
              the values of the new pivoted fields.
          groupby : List(str)
              The optional data fields to group by. If not specified, a single group containing
              all data objects will be used.
          limit : float
              An optional parameter indicating the maximum number of pivoted fields to generate.
              The default ( ``0`` ) applies no limit. The pivoted ``pivot`` names are sorted in
              ascending order prior to enforcing the limit.
              **Default value:** ``0``
          op : string
              The aggregation operation to apply to grouped ``value`` field values.
              **Default value:** ``sum``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_fold : fold transform - opposite of pivot.
          alt.PivotTransform : underlying transform object
          """
          return self._add_transform(
              core.PivotTransform(
                  pivot=pivot, value=value, groupby=groupby, limit=limit, op=op
              )
          )
  
      def transform_quantile(
          self,
          quantile,
          as_=Undefined,
          groupby=Undefined,
          probs=Undefined,
          step=Undefined,
      ):
          """Add a quantile transform to the chart
  
          Parameters
          ----------
          quantile : str
              The data field for which to perform quantile estimation.
          as : [str, str]
              The output field names for the probability and quantile values.
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          probs : List(float)
              An array of probabilities in the range (0, 1) for which to compute quantile values.
              If not specified, the *step* parameter will be used.
          step : float
              A probability step size (default 0.01) 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 *probs* parameter is not provided. **Default value:** ``["prob", "value"]``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.QuantileTransform : underlying transform object
          """
          return self._add_transform(
              core.QuantileTransform(
                  quantile=quantile,
                  groupby=groupby,
                  probs=probs,
                  step=step,
                  **{"as": as_},
              )
          )
  
      def transform_regression(
          self,
          on,
          regression,
          as_=Undefined,
          extent=Undefined,
          groupby=Undefined,
          method=Undefined,
          order=Undefined,
          params=Undefined,
      ):
          """Add a RegressionTransform to the chart.
  
          Parameters
          ----------
          on : str
              The data field of the independent variable to use a predictor.
          regression : str
              The data field of the dependent variable to predict.
          as_ : [str, str]
              The output field names for the smoothed points generated by the regression
              transform. **Default value:** The field names of the input x and y values.
          extent : [float, float]
              A [min, max] domain over the independent (x) field for the starting and ending
              points of the generated trend line.
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          method : enum('linear', 'log', 'exp', 'pow', 'quad', 'poly')
              The functional form of the regression model. One of ``"linear"``, ``"log"``,
              ``"exp"``, ``"pow"``, ``"quad"``, or ``"poly"``.  **Default value:** ``"linear"``
          order : float
              The polynomial order (number of coefficients) for the 'poly' method.
              **Default value:** ``3``
          params : boolean
              A boolean flag indicating if the transform should return the regression model
              parameters (one object per group), rather than trend line points.
              The resulting objects include a ``coef`` array of fitted coefficient values
              (starting with the intercept term and then including terms of increasing order)
              and an ``rSquared`` value (indicating the total variance explained by the model).
              **Default value:** ``false``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_loess : LOESS transform
          alt.RegressionTransform : underlying transform object
          """
          return self._add_transform(
              core.RegressionTransform(
                  regression=regression,
                  on=on,
                  extent=extent,
                  groupby=groupby,
                  method=method,
                  order=order,
                  params=params,
                  **{"as": as_},
              )
          )
  
      def transform_sample(self, sample=1000):
          """
          Add a SampleTransform to the schema.
  
          Parameters
          ----------
          sample : float
              The maximum number of data objects to include in the sample. Default: 1000.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.SampleTransform : underlying transform object
          """
          return self._add_transform(core.SampleTransform(sample))
  
      def transform_stack(self, as_, stack, groupby, offset=Undefined, sort=Undefined):
          """
          Add a StackTransform to the schema.
  
          Parameters
          ----------
          as_ : anyOf(string, List(string))
              Output field names. This can be either a string or an array of strings with
              two elements denoting the name for the fields for stack start and stack end
              respectively.
              If a single string(eg."val") is provided, the end field will be "val_end".
          stack : string
              The field which is stacked.
          groupby : List(string)
              The data fields to group by.
          offset : enum('zero', 'center', 'normalize')
              Mode for stacking marks. Default: 'zero'.
          sort : List(:class:`SortField`)
              Field that determines the order of leaves in the stacked charts.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.StackTransform : underlying transform object
          """
          return self._add_transform(
              core.StackTransform(
                  stack=stack, groupby=groupby, offset=offset, sort=sort, **{"as": as_}
              )
          )
  
      def transform_timeunit(
          self, as_=Undefined, field=Undefined, timeUnit=Undefined, **kwargs
      ):
          """
          Add a TimeUnitTransform to the schema.
  
          Parameters
          ----------
          as_ : string
              The output field to write the timeUnit value.
          field : string
              The data field to apply time unit.
          timeUnit : :class:`TimeUnit`
              The timeUnit.
          **kwargs
              transforms can also be passed by keyword argument; see Examples
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> from altair import datum, expr
  
          >>> chart = alt.Chart().transform_timeunit(month='month(date)')
          >>> chart.transform[0]
          TimeUnitTransform({
            as: 'month',
            field: 'date',
            timeUnit: 'month'
          })
  
          It's also possible to pass the ``TimeUnitTransform`` arguments directly;
          this is most useful in cases where the desired field name is not a
          valid python identifier:
  
          >>> kwds = {'as': 'month', 'timeUnit': 'month', 'field': 'The Month'}
          >>> chart = alt.Chart().transform_timeunit(**kwds)
          >>> chart.transform[0]
          TimeUnitTransform({
            as: 'month',
            field: 'The Month',
            timeUnit: 'month'
          })
  
          As the first form is easier to write and understand, that is the
          recommended method.
  
          See Also
          --------
          alt.TimeUnitTransform : underlying transform object
  
          """
          if as_ is Undefined:
              as_ = kwargs.pop("as", Undefined)
          else:
              if "as" in kwargs:
                  raise ValueError(
                      "transform_timeunit: both 'as_' and 'as' passed as arguments."
                  )
          if as_ is not Undefined:
              dct = {"as": as_, "timeUnit": timeUnit, "field": field}
              self = self._add_transform(core.TimeUnitTransform(**dct))
          for as_, shorthand in kwargs.items():
              dct = utils.parse_shorthand(
                  shorthand,
                  parse_timeunits=True,
                  parse_aggregates=False,
                  parse_types=False,
              )
              dct.pop("type", None)
              dct["as"] = as_
              if "timeUnit" not in dct:
                  raise ValueError("'{}' must include a valid timeUnit".format(shorthand))
              self = self._add_transform(core.TimeUnitTransform(**dct))
          return self
  
      def transform_window(
          self,
          window=Undefined,
          frame=Undefined,
          groupby=Undefined,
          ignorePeers=Undefined,
          sort=Undefined,
          **kwargs,
      ):
          """Add a WindowTransform to the schema
  
          Parameters
          ----------
          window : List(:class:`WindowFieldDef`)
              The definition of the fields in the window, and what calculations to use.
          frame : List(anyOf(None, float))
              A frame specification as a two-element array indicating how the sliding window
              should proceed. The array entries should either be a number indicating the offset
              from the current data object, or null to indicate unbounded rows preceding or
              following the current data object. The default value is ``[null, 0]``, indicating
              that the sliding window includes the current object and all preceding objects. The
              value ``[-5, 5]`` indicates that the window should include five objects preceding
              and five objects following the current object. Finally, ``[null, null]`` indicates
              that the window frame should always include all data objects. The only operators
              affected are the aggregation operations and the ``first_value``, ``last_value``, and
              ``nth_value`` window operations. The other window operations are not affected by
              this.
  
              **Default value:** :  ``[null, 0]`` (includes the current object and all preceding
              objects)
          groupby : List(string)
              The data fields for partitioning the data objects into separate windows. If
              unspecified, all data points will be in a single group.
          ignorePeers : boolean
              Indicates if the sliding window frame should ignore peer values. (Peer values are
              those considered identical by the sort criteria). The default is false, causing the
              window frame to expand to include all peer values. If set to true, the window frame
              will be defined by offset values only. This setting only affects those operations
              that depend on the window frame, namely aggregation operations and the first_value,
              last_value, and nth_value window operations.
  
              **Default value:** ``false``
          sort : List(:class:`SortField`)
              A sort field definition for sorting data objects within a window. If two data
              objects are considered equal by the comparator, they are considered “peer” values of
              equal rank. If sort is not specified, the order is undefined: data objects are
              processed in the order they are observed and none are considered peers (the
              ignorePeers parameter is ignored and treated as if set to ``true`` ).
          **kwargs
              transforms can also be passed by keyword argument; see Examples
  
          Examples
          --------
          A cumulative line chart
  
          >>> import altair as alt
          >>> import numpy as np
          >>> import pandas as pd
          >>> data = pd.DataFrame({'x': np.arange(100),
          ...                      'y': np.random.randn(100)})
          >>> chart = alt.Chart(data).mark_line().encode(
          ...     x='x:Q',
          ...     y='ycuml:Q'
          ... ).transform_window(
          ...     ycuml='sum(y)'
          ... )
          >>> chart.transform[0]
          WindowTransform({
            window: [WindowFieldDef({
              as: 'ycuml',
              field: 'y',
              op: 'sum'
            })]
          })
  
          """
          if kwargs:
              if window is Undefined:
                  window = []
              for as_, shorthand in kwargs.items():
                  kwds = {"as": as_}
                  kwds.update(
                      utils.parse_shorthand(
                          shorthand,
                          parse_aggregates=False,
                          parse_window_ops=True,
                          parse_timeunits=False,
                          parse_types=False,
                      )
                  )
                  window.append(core.WindowFieldDef(**kwds))
  
          return self._add_transform(
              core.WindowTransform(
                  window=window,
                  frame=frame,
                  groupby=groupby,
                  ignorePeers=ignorePeers,
                  sort=sort,
              )
          )
  
      # Display-related methods
  
      def _repr_mimebundle_(self, include=None, exclude=None):
          """Return a MIME bundle for display in Jupyter frontends."""
          # Catch errors explicitly to get around issues in Jupyter frontend
          # see https://github.com/ipython/ipython/issues/11038
          try:
              dct = self.to_dict()
          except Exception:
              utils.display_traceback(in_ipython=True)
              return {}
          else:
              return renderers.get()(dct)
  
      def display(self, renderer=Undefined, theme=Undefined, actions=Undefined, **kwargs):
          """Display chart in Jupyter notebook or JupyterLab
  
          Parameters are passed as options to vega-embed within supported frontends.
          See https://github.com/vega/vega-embed#options for details.
  
          Parameters
          ----------
          renderer : string ('canvas' or 'svg')
              The renderer to use
          theme : string
              The Vega theme name to use; see https://github.com/vega/vega-themes
          actions : bool or dict
              Specify whether action links ("Open In Vega Editor", etc.) are
              included in the view.
          **kwargs :
              Additional parameters are also passed to vega-embed as options.
  
          """
          from IPython.display import display
  
          if renderer is not Undefined:
              kwargs["renderer"] = renderer
          if theme is not Undefined:
              kwargs["theme"] = theme
          if actions is not Undefined:
              kwargs["actions"] = actions
  
          if kwargs:
              options = renderers.options.copy()
              options["embed_options"] = options.get("embed_options", {}).copy()
              options["embed_options"].update(kwargs)
              with renderers.enable(**options):
                  display(self)
          else:
              display(self)
  
      @utils.deprecation.deprecated(message="serve() is deprecated. Use show() instead.")
      def serve(
          self,
          ip="127.0.0.1",
          port=8888,
          n_retries=50,
          files=None,
          jupyter_warning=True,
          open_browser=True,
          http_server=None,
          **kwargs,
      ):
          """Open a browser window and display a rendering of the chart
  
          Parameters
          ----------
          html : string
              HTML to serve
          ip : string (default = '127.0.0.1')
              ip address at which the HTML will be served.
          port : int (default = 8888)
              the port at which to serve the HTML
          n_retries : int (default = 50)
              the number of nearby ports to search if the specified port
              is already in use.
          files : dictionary (optional)
              dictionary of extra content to serve
          jupyter_warning : bool (optional)
              if True (default), then print a warning if this is used
              within the Jupyter notebook
          open_browser : bool (optional)
              if True (default), then open a web browser to the given HTML
          http_server : class (optional)
              optionally specify an HTTPServer class to use for showing the
              figure. The default is Python's basic HTTPServer.
          **kwargs :
              additional keyword arguments passed to the save() method
  
          """
          from ...utils.server import serve
  
          html = io.StringIO()
          self.save(html, format="html", **kwargs)
          html.seek(0)
  
          serve(
              html.read(),
              ip=ip,
              port=port,
              n_retries=n_retries,
              files=files,
              jupyter_warning=jupyter_warning,
              open_browser=open_browser,
              http_server=http_server,
          )
  
      def show(self, embed_opt=None, open_browser=None):
          """Show the chart in an external browser window.
  
          This requires a recent version of the altair_viewer package.
  
          Parameters
          ----------
          embed_opt : dict (optional)
              The Vega embed options that control the dispay of the chart.
          open_browser : bool (optional)
              Specify whether a browser window should be opened. If not specified,
              a browser window will be opened only if the server is not already
              connected to a browser.
          """
          try:
              import altair_viewer  # type: ignore
          except ImportError:
              raise ValueError(
                  "show() method requires the altair_viewer package. "
                  "See http://github.com/altair-viz/altair_viewer"
              )
          altair_viewer.show(self, embed_opt=embed_opt, open_browser=open_browser)
  
      @utils.use_signature(core.Resolve)
      def _set_resolve(self, **kwargs):
          """Copy the chart and update the resolve property with kwargs"""
          if not hasattr(self, "resolve"):
              raise ValueError(
                  "{} object has no attribute " "'resolve'".format(self.__class__)
              )
          copy = self.copy(deep=["resolve"])
          if copy.resolve is Undefined:
              copy.resolve = core.Resolve()
          for key, val in kwargs.items():
              copy.resolve[key] = val
          return copy
  
      @utils.use_signature(core.AxisResolveMap)
      def resolve_axis(self, *args, **kwargs):
          return self._set_resolve(axis=core.AxisResolveMap(*args, **kwargs))
  
      @utils.use_signature(core.LegendResolveMap)
      def resolve_legend(self, *args, **kwargs):
          return self._set_resolve(legend=core.LegendResolveMap(*args, **kwargs))
  
      @utils.use_signature(core.ScaleResolveMap)
      def resolve_scale(self, *args, **kwargs):
          return self._set_resolve(scale=core.ScaleResolveMap(*args, **kwargs))
  
  
  class _EncodingMixin(object):
      @utils.use_signature(core.FacetedEncoding)
      def encode(self, *args, **kwargs):
          # Convert args to kwargs based on their types.
          kwargs = utils.infer_encoding_types(args, kwargs, channels)
  
          # get a copy of the dict representation of the previous encoding
          copy = self.copy(deep=["encoding"])
          encoding = copy._get("encoding", {})
          if isinstance(encoding, core.VegaLiteSchema):
              encoding = {k: v for k, v in encoding._kwds.items() if v is not Undefined}
  
          # update with the new encodings, and apply them to the copy
          encoding.update(kwargs)
          copy.encoding = core.FacetedEncoding(**encoding)
          return copy
  
      def facet(
          self,
          facet=Undefined,
          row=Undefined,
          column=Undefined,
          data=Undefined,
          columns=Undefined,
          **kwargs,
      ):
          """Create a facet chart from the current chart.
  
          Faceted charts require data to be specified at the top level; if data
          is not specified, the data from the current chart will be used at the
          top level.
  
          Parameters
          ----------
          facet : string or alt.Facet (optional)
              The data column to use as an encoding for a wrapped facet.
              If specified, then neither row nor column may be specified.
          column : string or alt.Column (optional)
              The data column to use as an encoding for a column facet.
              May be combined with row argument, but not with facet argument.
          row : string or alt.Column (optional)
              The data column to use as an encoding for a row facet.
              May be combined with column argument, but not with facet argument.
          data : string or dataframe (optional)
              The dataset to use for faceting. If not supplied, then data must
              be specified in the top-level chart that calls this method.
          columns : integer
              the maximum number of columns for a wrapped facet.
  
          Returns
          -------
          self :
              for chaining
          """
          facet_specified = facet is not Undefined
          rowcol_specified = row is not Undefined or column is not Undefined
  
          if facet_specified and rowcol_specified:
              raise ValueError(
                  "facet argument cannot be combined with row/column argument."
              )
  
          if data is Undefined:
              if self.data is Undefined:
                  raise ValueError(
                      "Facet charts require data to be specified at the top level."
                  )
              self = self.copy(deep=False)
              data, self.data = self.data, Undefined
  
          if facet_specified:
              if isinstance(facet, str):
                  facet = channels.Facet(facet)
          else:
              facet = FacetMapping(row=row, column=column)
  
          return FacetChart(spec=self, facet=facet, data=data, columns=columns, **kwargs)
  
  
  class Chart(
      TopLevelMixin, _EncodingMixin, mixins.MarkMethodMixin, core.TopLevelUnitSpec
  ):
      """Create a basic Altair/Vega-Lite chart.
  
      Although it is possible to set all Chart properties as constructor attributes,
      it is more idiomatic to use methods such as ``mark_point()``, ``encode()``,
      ``transform_filter()``, ``properties()``, etc. See Altair's documentation
      for details and examples: http://altair-viz.github.io/.
  
      Attributes
      ----------
      data : Data
          An object describing the data source
      mark : AnyMark
          A string describing the mark type (one of `"bar"`, `"circle"`, `"square"`, `"tick"`,
           `"line"`, * `"area"`, `"point"`, `"rule"`, `"geoshape"`, and `"text"`) or a
           MarkDef object.
      encoding : FacetedEncoding
          A key-value mapping between encoding channels and definition of fields.
      autosize : anyOf(AutosizeType, AutoSizeParams)
          Sets how the visualization size should be determined. If a string, should be one of
          `"pad"`, `"fit"` or `"none"`. Object values can additionally specify parameters for
          content sizing and automatic resizing. `"fit"` is only supported for single and
          layered views that don't use `rangeStep`.  __Default value__: `pad`
      background : string
          CSS color property to use as the background of visualization.
  
          **Default value:** none (transparent)
      config : Config
          Vega-Lite configuration object.  This property can only be defined at the top-level
          of a specification.
      description : string
          Description of this mark for commenting purpose.
      height : float
          The height of a visualization.
      name : string
          Name of the visualization for later reference.
      padding : Padding
          The default visualization padding, in pixels, from the edge of the visualization
          canvas to the data rectangle.  If a number, specifies padding for all sides. If an
          object, the value should have the format `{"left": 5, "top": 5, "right": 5,
          "bottom": 5}` to specify padding for each side of the visualization.  __Default
          value__: `5`
      projection : Projection
          An object defining properties of geographic projection.  Works with `"geoshape"`
          marks and `"point"` or `"line"` marks that have a channel (one or more of `"X"`,
          `"X2"`, `"Y"`, `"Y2"`) with type `"latitude"`, or `"longitude"`.
      selection : Mapping(required=[])
          A key-value mapping between selection names and definitions.
      title : anyOf(string, TitleParams)
          Title for the plot.
      transform : List(Transform)
          An array of data transformations such as filter and new field calculation.
      width : float
          The width of a visualization.
      """
  
      def __init__(
          self,
          data=Undefined,
          encoding=Undefined,
          mark=Undefined,
          width=Undefined,
          height=Undefined,
          **kwargs,
      ):
          super(Chart, self).__init__(
              data=data,
              encoding=encoding,
              mark=mark,
              width=width,
              height=height,
              **kwargs,
          )
  
      @classmethod
      def from_dict(cls, dct, validate=True):
          """Construct class from a dictionary representation
  
          Parameters
          ----------
          dct : dictionary
              The dict from which to construct the class
          validate : boolean
              If True (default), then validate the input against the schema.
  
          Returns
          -------
          obj : Chart object
              The wrapped schema
  
          Raises
          ------
          jsonschema.ValidationError :
              if validate=True and dct does not conform to the schema
          """
          for class_ in TopLevelMixin.__subclasses__():
              if class_ is Chart:
                  class_ = super(Chart, cls)
              try:
                  return class_.from_dict(dct, validate=validate)
              except jsonschema.ValidationError:
                  pass
  
          # As a last resort, try using the Root vegalite object
          return core.Root.from_dict(dct, validate)
  
      def to_dict(self, *args, **kwargs):
          """Convert the chart to a dictionary suitable for JSON export."""
          context = kwargs.get("context", {})
          if self.data is Undefined and "data" not in context:
              # No data specified here or in parent: inject empty data
              # for easier specification of datum encodings.
              copy = self.copy(deep=False)
              copy.data = core.InlineData(values=[{}])
              return super(Chart, copy).to_dict(*args, **kwargs)
          return super().to_dict(*args, **kwargs)
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *params):
          warnings.warn(
              """'add_selection' is deprecated. Use 'add_parameter'.""",
              DeprecationWarning,
          )
          return self.add_parameter(*params)
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          encodings = []
          if bind_x:
              encodings.append("x")
          if bind_y:
              encodings.append("y")
          return self.add_selection(
              selection_interval(bind="scales", encodings=encodings)
          )
  
  
  def _check_if_valid_subspec(spec, classname):
      """Check if the spec is a valid sub-spec.
  
      If it is not, then raise a ValueError
      """
      err = (
          'Objects with "{0}" attribute cannot be used within {1}. '
          "Consider defining the {0} attribute in the {1} object instead."
      )
  
      if not isinstance(spec, (core.SchemaBase, dict)):
          raise ValueError("Only chart objects can be used in {0}.".format(classname))
      for attr in TOPLEVEL_ONLY_KEYS:
          if isinstance(spec, core.SchemaBase):
              val = getattr(spec, attr, Undefined)
          else:
              val = spec.get(attr, Undefined)
          if val is not Undefined:
              raise ValueError(err.format(attr, classname))
  
  
  def _check_if_can_be_layered(spec):
      """Check if the spec can be layered."""
  
      def _get(spec, attr):
          if isinstance(spec, core.SchemaBase):
              return spec._get(attr)
          else:
              return spec.get(attr, Undefined)
  
      encoding = _get(spec, "encoding")
      if encoding is not Undefined:
          for channel in ["row", "column", "facet"]:
              if _get(encoding, channel) is not Undefined:
                  raise ValueError("Faceted charts cannot be layered.")
      if isinstance(spec, (Chart, LayerChart)):
          return
  
      if not isinstance(spec, (core.SchemaBase, dict)):
          raise ValueError("Only chart objects can be layered.")
      if _get(spec, "facet") is not Undefined:
          raise ValueError("Faceted charts cannot be layered.")
      if isinstance(spec, FacetChart) or _get(spec, "facet") is not Undefined:
          raise ValueError("Faceted charts cannot be layered.")
      if isinstance(spec, RepeatChart) or _get(spec, "repeat") is not Undefined:
          raise ValueError("Repeat charts cannot be layered.")
      if isinstance(spec, ConcatChart) or _get(spec, "concat") is not Undefined:
          raise ValueError("Concatenated charts cannot be layered.")
      if isinstance(spec, HConcatChart) or _get(spec, "hconcat") is not Undefined:
          raise ValueError("Concatenated charts cannot be layered.")
      if isinstance(spec, VConcatChart) or _get(spec, "vconcat") is not Undefined:
          raise ValueError("Concatenated charts cannot be layered.")
  
  
  @utils.use_signature(core.TopLevelRepeatSpec)
  class RepeatChart(TopLevelMixin, core.TopLevelRepeatSpec):
      """A chart repeated across rows and columns with small changes"""
  
      # Because TopLevelRepeatSpec is defined as a union as of Vega-Lite schema 4.9,
      # we set the arguments explicitly here.
      # TODO: Should we instead use tools/schemapi/codegen._get_args?
      def __init__(
          self,
          repeat=Undefined,
          spec=Undefined,
          align=Undefined,
          autosize=Undefined,
          background=Undefined,
          bounds=Undefined,
          center=Undefined,
          columns=Undefined,
          config=Undefined,
          data=Undefined,
          datasets=Undefined,
          description=Undefined,
          name=Undefined,
          padding=Undefined,
          params=Undefined,
          resolve=Undefined,
          spacing=Undefined,
          title=Undefined,
          transform=Undefined,
          usermeta=Undefined,
          **kwds,
      ):
          _check_if_valid_subspec(spec, "RepeatChart")
          super(RepeatChart, self).__init__(
              repeat=repeat,
              spec=spec,
              align=align,
              autosize=autosize,
              background=background,
              bounds=bounds,
              center=center,
              columns=columns,
              config=config,
              data=data,
              datasets=datasets,
              description=description,
              name=name,
              padding=padding,
              params=params,
              resolve=resolve,
              spacing=spacing,
              title=title,
              transform=transform,
              usermeta=usermeta,
              **kwds,
          )
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          copy = self.copy(deep=False)
          copy.spec = copy.spec.interactive(name=name, bind_x=bind_x, bind_y=bind_y)
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to the chart."""
          if not selections or self.spec is Undefined:
              return self
          copy = self.copy()
          copy.spec = copy.spec.add_selection(*selections)
          return copy
  
  
  def repeat(repeater="repeat"):
      """Tie a channel to the row or column within a repeated chart
  
      The output of this should be passed to the ``field`` attribute of
      a channel.
  
      Parameters
      ----------
      repeater : {'row'|'column'|'repeat'|'layer'}
          The repeater to tie the field to. Default is 'repeat'.
  
      Returns
      -------
      repeat : RepeatRef object
      """
      if repeater not in ["row", "column", "repeat", "layer"]:
          raise ValueError("repeater must be one of ['row', 'column', 'repeat', 'layer']")
      return core.RepeatRef(repeat=repeater)
  
  
  @utils.use_signature(core.TopLevelConcatSpec)
  class ConcatChart(TopLevelMixin, core.TopLevelConcatSpec):
      """A chart with horizontally-concatenated facets"""
  
      def __init__(self, data=Undefined, concat=(), columns=Undefined, **kwargs):
          # TODO: move common data to top level?
          for spec in concat:
              _check_if_valid_subspec(spec, "ConcatChart")
          super(ConcatChart, self).__init__(
              data=data, concat=list(concat), columns=columns, **kwargs
          )
          self.data, self.concat = _combine_subchart_data(self.data, self.concat)
  
      def __ior__(self, other):
          _check_if_valid_subspec(other, "ConcatChart")
          self.concat.append(other)
          self.data, self.concat = _combine_subchart_data(self.data, self.concat)
          return self
  
      def __or__(self, other):
          copy = self.copy(deep=["concat"])
          copy |= other
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.concat:
              return self
          copy = self.copy()
          copy.concat = [chart.add_selection(*selections) for chart in copy.concat]
          return copy
  
  
  def concat(*charts, **kwargs):
      """Concatenate charts horizontally"""
      return ConcatChart(concat=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelHConcatSpec)
  class HConcatChart(TopLevelMixin, core.TopLevelHConcatSpec):
      """A chart with horizontally-concatenated facets"""
  
      def __init__(self, data=Undefined, hconcat=(), **kwargs):
          # TODO: move common data to top level?
          for spec in hconcat:
              _check_if_valid_subspec(spec, "HConcatChart")
          super(HConcatChart, self).__init__(data=data, hconcat=list(hconcat), **kwargs)
          self.data, self.hconcat = _combine_subchart_data(self.data, self.hconcat)
  
      def __ior__(self, other):
          _check_if_valid_subspec(other, "HConcatChart")
          self.hconcat.append(other)
          self.data, self.hconcat = _combine_subchart_data(self.data, self.hconcat)
          return self
  
      def __or__(self, other):
          copy = self.copy(deep=["hconcat"])
          copy |= other
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.hconcat:
              return self
          copy = self.copy()
          copy.hconcat = [chart.add_selection(*selections) for chart in copy.hconcat]
          return copy
  
  
  def hconcat(*charts, **kwargs):
      """Concatenate charts horizontally"""
      return HConcatChart(hconcat=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelVConcatSpec)
  class VConcatChart(TopLevelMixin, core.TopLevelVConcatSpec):
      """A chart with vertically-concatenated facets"""
  
      def __init__(self, data=Undefined, vconcat=(), **kwargs):
          # TODO: move common data to top level?
          for spec in vconcat:
              _check_if_valid_subspec(spec, "VConcatChart")
          super(VConcatChart, self).__init__(data=data, vconcat=list(vconcat), **kwargs)
          self.data, self.vconcat = _combine_subchart_data(self.data, self.vconcat)
  
      def __iand__(self, other):
          _check_if_valid_subspec(other, "VConcatChart")
          self.vconcat.append(other)
          self.data, self.vconcat = _combine_subchart_data(self.data, self.vconcat)
          return self
  
      def __and__(self, other):
          copy = self.copy(deep=["vconcat"])
          copy &= other
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.vconcat:
              return self
          copy = self.copy()
          copy.vconcat = [chart.add_selection(*selections) for chart in copy.vconcat]
          return copy
  
  
  def vconcat(*charts, **kwargs):
      """Concatenate charts vertically"""
      return VConcatChart(vconcat=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelLayerSpec)
  class LayerChart(TopLevelMixin, _EncodingMixin, core.TopLevelLayerSpec):
      """A Chart with layers within a single panel"""
  
      def __init__(self, data=Undefined, layer=(), **kwargs):
          # TODO: move common data to top level?
          # TODO: check for conflicting interaction
          for spec in layer:
              _check_if_valid_subspec(spec, "LayerChart")
              _check_if_can_be_layered(spec)
          super(LayerChart, self).__init__(data=data, layer=list(layer), **kwargs)
          self.data, self.layer = _combine_subchart_data(self.data, self.layer)
  
          # Some properties are not allowed within layer; we'll move to parent.
          layer_props = ("height", "width", "view")
          combined_dict, self.layer = _remove_layer_props(self, self.layer, layer_props)
  
          for prop in combined_dict:
              self[prop] = combined_dict[prop]
  
      def __iadd__(self, other):
          _check_if_valid_subspec(other, "LayerChart")
          _check_if_can_be_layered(other)
          self.layer.append(other)
          self.data, self.layer = _combine_subchart_data(self.data, self.layer)
          return self
  
      def __add__(self, other):
          copy = self.copy(deep=["layer"])
          copy += other
          return copy
  
      def add_layers(self, *layers):
          copy = self.copy(deep=["layer"])
          for layer in layers:
              copy += layer
          return copy
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          if not self.layer:
              raise ValueError(
                  "LayerChart: cannot call interactive() until a " "layer is defined"
              )
          copy = self.copy(deep=["layer"])
          copy.layer[0] = copy.layer[0].interactive(
              name=name, bind_x=bind_x, bind_y=bind_y
          )
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.layer:
              return self
          copy = self.copy()
          copy.layer[0] = copy.layer[0].add_selection(*selections)
          return copy
  
  
  def layer(*charts, **kwargs):
      """layer multiple charts"""
      return LayerChart(layer=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelFacetSpec)
  class FacetChart(TopLevelMixin, core.TopLevelFacetSpec):
      """A Chart with layers within a single panel"""
  
      def __init__(self, data=Undefined, spec=Undefined, facet=Undefined, **kwargs):
          _check_if_valid_subspec(spec, "FacetChart")
          super(FacetChart, self).__init__(data=data, spec=spec, facet=facet, **kwargs)
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          copy = self.copy(deep=False)
          copy.spec = copy.spec.interactive(name=name, bind_x=bind_x, bind_y=bind_y)
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to the chart."""
          if not selections or self.spec is Undefined:
              return self
          copy = self.copy()
          copy.spec = copy.spec.add_selection(*selections)
          return copy
  
  
  def topo_feature(url, feature, **kwargs):
      """A convenience function for extracting features from a topojson url
  
      Parameters
      ----------
      url : string
          An URL from which to load the data set.
  
      feature : string
          The name of the TopoJSON object set to convert to a GeoJSON feature collection. For
          example, in a map of the world, there may be an object set named `"countries"`.
          Using the feature property, we can extract this set and generate a GeoJSON feature
          object for each country.
  
      **kwargs :
          additional keywords passed to TopoDataFormat
      """
      return core.UrlData(
          url=url, format=core.TopoDataFormat(type="topojson", feature=feature, **kwargs)
      )
  
  
  def _combine_subchart_data(data, subcharts):
      def remove_data(subchart):
          if subchart.data is not Undefined:
              subchart = subchart.copy()
              subchart.data = Undefined
          return subchart
  
      if not subcharts:
          # No subcharts = nothing to do.
          pass
      elif data is Undefined:
          # Top level has no data; all subchart data must
          # be identical to proceed.
          subdata = subcharts[0].data
          if subdata is not Undefined and all(c.data is subdata for c in subcharts):
              data = subdata
              subcharts = [remove_data(c) for c in subcharts]
      else:
          # Top level has data; subchart data must be either
          # undefined or identical to proceed.
          if all(c.data is Undefined or c.data is data for c in subcharts):
              subcharts = [remove_data(c) for c in subcharts]
  
      return data, subcharts
  
  
  def _remove_layer_props(chart, subcharts, layer_props):
      def remove_prop(subchart, prop):
          # If subchart is a UnitSpec, then subchart["height"] raises a KeyError
          try:
              if subchart[prop] is not Undefined:
                  subchart = subchart.copy()
                  subchart[prop] = Undefined
          except KeyError:
              pass
          return subchart
  
      output_dict = {}
  
      if not subcharts:
          # No subcharts = nothing to do.
          return output_dict, subcharts
  
      for prop in layer_props:
          if chart[prop] is Undefined:
              # Top level does not have this prop.
              # Check for consistent props within the subcharts.
              values = []
              for c in subcharts:
                  # If c is a UnitSpec, then c["height"] raises a KeyError.
                  try:
                      val = c[prop]
                      if val is not Undefined:
                          values.append(val)
                  except KeyError:
                      pass
              if len(values) == 0:
                  pass
              elif all(v is values[0] for v in values[1:]):
                  output_dict[prop] = values[0]
              else:
                  raise ValueError(f"There are inconsistent values {values} for {prop}")
          else:
              # Top level has this prop; subchart props must be either
              # Undefined or identical to proceed.
              if all(c[prop] is Undefined or c[prop] is chart[prop] for c in subcharts):
                  output_dict[prop] = chart[prop]
              else:
                  raise ValueError(f"There are inconsistent values {values} for {prop}")
          subcharts = [remove_prop(c, prop) for c in subcharts]
  
      return output_dict, subcharts
  
  
  @utils.use_signature(core.SequenceParams)
  def sequence(start, stop=None, step=Undefined, as_=Undefined, **kwds):
      """Sequence generator."""
      if stop is None:
          start, stop = 0, start
      params = core.SequenceParams(start=start, stop=stop, step=step, **{"as": as_})
      return core.SequenceGenerator(sequence=params, **kwds)
  
  
  @utils.use_signature(core.GraticuleParams)
  def graticule(**kwds):
      """Graticule generator."""
      if not kwds:
          # graticule: True indicates default parameters
          graticule = True
      else:
          graticule = core.GraticuleParams(**kwds)
      return core.GraticuleGenerator(graticule=graticule)
  
  
  def sphere():
      """Sphere generator."""
      return core.SphereGenerator(sphere=True)
  
files: altair/vegalite/v5/api.py
files: import warnings
  
  import hashlib
  import io
  import json
  import jsonschema
  import pandas as pd
  from toolz.curried import pipe as _pipe
  
  from .schema import core, channels, mixins, Undefined, SCHEMA_URL
  
  from .data import data_transformers
  from ... import utils, expr
  from .display import renderers, VEGALITE_VERSION, VEGAEMBED_VERSION, VEGA_VERSION
  from .theme import themes
  
  
  # ------------------------------------------------------------------------
  # Data Utilities
  def _dataset_name(values):
      """Generate a unique hash of the data
  
      Parameters
      ----------
      values : list or dict
          A list/dict representation of data values.
  
      Returns
      -------
      name : string
          A unique name generated from the hash of the values.
      """
      if isinstance(values, core.InlineDataset):
          values = values.to_dict()
      if values == [{}]:
          return "empty"
      values_json = json.dumps(values, sort_keys=True)
      hsh = hashlib.md5(values_json.encode()).hexdigest()
      return "data-" + hsh
  
  
  def _consolidate_data(data, context):
      """If data is specified inline, then move it to context['datasets']
  
      This function will modify context in-place, and return a new version of data
      """
      values = Undefined
      kwds = {}
  
      if isinstance(data, core.InlineData):
          if data.name is Undefined and data.values is not Undefined:
              if isinstance(data.values, core.InlineDataset):
                  values = data.to_dict()["values"]
              else:
                  values = data.values
              kwds = {"format": data.format}
  
      elif isinstance(data, dict):
          if "name" not in data and "values" in data:
              values = data["values"]
              kwds = {k: v for k, v in data.items() if k != "values"}
  
      if values is not Undefined:
          name = _dataset_name(values)
          data = core.NamedData(name=name, **kwds)
          context.setdefault("datasets", {})[name] = values
  
      return data
  
  
  def _prepare_data(data, context=None):
      """Convert input data to data for use within schema
  
      Parameters
      ----------
      data :
          The input dataset in the form of a DataFrame, dictionary, altair data
          object, or other type that is recognized by the data transformers.
      context : dict (optional)
          The to_dict context in which the data is being prepared. This is used
          to keep track of information that needs to be passed up and down the
          recursive serialization routine, such as global named datasets.
      """
      if data is Undefined:
          return data
  
      # convert dataframes  or objects with __geo_interface__ to dict
      if isinstance(data, pd.DataFrame) or hasattr(data, "__geo_interface__"):
          data = _pipe(data, data_transformers.get())
  
      # convert string input to a URLData
      if isinstance(data, str):
          data = core.UrlData(data)
  
      # consolidate inline data to top-level datasets
      if context is not None and data_transformers.consolidate_datasets:
          data = _consolidate_data(data, context)
  
      # if data is still not a recognized type, then return
      if not isinstance(data, (dict, core.Data)):
          warnings.warn("data of type {} not recognized".format(type(data)))
  
      return data
  
  
  # ------------------------------------------------------------------------
  # Aliases & specializations
  Bin = core.BinParams
  
  
  @utils.use_signature(core.LookupData)
  class LookupData(core.LookupData):
      def to_dict(self, *args, **kwargs):
          """Convert the chart to a dictionary suitable for JSON export."""
          copy = self.copy(deep=False)
          copy.data = _prepare_data(copy.data, kwargs.get("context"))
          return super(LookupData, copy).to_dict(*args, **kwargs)
  
  
  @utils.use_signature(core.FacetMapping)
  class FacetMapping(core.FacetMapping):
      _class_is_valid_at_instantiation = False
  
      def to_dict(self, *args, **kwargs):
          copy = self.copy(deep=False)
          context = kwargs.get("context", {})
          data = context.get("data", None)
          if isinstance(self.row, str):
              copy.row = core.FacetFieldDef(**utils.parse_shorthand(self.row, data))
          if isinstance(self.column, str):
              copy.column = core.FacetFieldDef(**utils.parse_shorthand(self.column, data))
          return super(FacetMapping, copy).to_dict(*args, **kwargs)
  
  
  # ------------------------------------------------------------------------
  # Encoding will contain channel objects that aren't valid at instantiation
  core.FacetedEncoding._class_is_valid_at_instantiation = False
  
  # ------------------------------------------------------------------------
  # These are parameters that are valid at the top level, but are not valid
  # for specs that are within a composite chart
  # (layer, hconcat, vconcat, facet, repeat)
  TOPLEVEL_ONLY_KEYS = {"background", "config", "autosize", "padding", "$schema"}
  
  
  def _get_channels_mapping():
      mapping = {}
      for attr in dir(channels):
          cls = getattr(channels, attr)
          if isinstance(cls, type) and issubclass(cls, core.SchemaBase):
              mapping[cls] = attr.replace("Value", "").lower()
      return mapping
  
  
  # -------------------------------------------------------------------------
  # Tools for working with parameters
  class Parameter(expr.core.OperatorMixin, object):
      """A Parameter object"""
  
      _counter = 0
  
      @classmethod
      def _get_name(cls):
          cls._counter += 1
          return "parameter{:03d}".format(cls._counter)
  
      def __init__(self, name):
          if name is None:
              name = self._get_name()
          self.name = name
  
      def to_dict(self):
          if self.param_type == "variable":
              if self.param.expr is Undefined:
                  return {"expr": self.name}
              else:
                  return {"expr": repr(self.param.expr)}
          elif self.param_type == "selection":
              return {
                  "param": self.name.to_dict()
                  if hasattr(self.name, "to_dict")
                  else self.name
              }
  
      def __invert__(self):
          if self.param_type == "selection":
              return core.PredicateComposition({"not": {"param": self.name}})
          else:
              return expr.core.OperatorMixin.__invert__(self)
  
      def __and__(self, other):
          if self.param_type == "selection":
              if isinstance(other, Parameter):
                  other = {"param": other.name}
              return core.PredicateComposition({"and": [{"param": self.name}, other]})
          else:
              return expr.core.OperatorMixin.__and__(self, other)
  
      def __or__(self, other):
          if self.param_type == "selection":
              if isinstance(other, Parameter):
                  other = {"param": other.name}
              return core.PredicateComposition({"or": [{"param": self.name}, other]})
          else:
              return expr.core.OperatorMixin.__or__(self, other)
  
      def __repr__(self):
          return "Parameter({0!r}, {1})".format(self.name, self.param)
  
      def _to_expr(self):
          if not hasattr(self.param, "expr") or self.param.expr is Undefined:
              return self.name
          else:
              return self.param.expr
  
      def _from_expr(self, expr):
          return parameter(expr=expr)
  
      def __getattr__(self, field_name):
          if field_name.startswith("__") and field_name.endswith("__"):
              raise AttributeError(field_name)
          _attrexpr = expr.core.GetAttrExpression(self.name, field_name)
          # If self is a SelectionParameter and field_name is in its
          # fields or encodings list, then we want to return an expression.
          if check_fields_and_encodings(self, field_name):
              return SelectionExpression(_attrexpr)
          return expr.core.GetAttrExpression(self.name, field_name)
  
  
  class SelectionExpression(expr.core.OperatorMixin, object):
      def __init__(self, expr):
          self.expr = expr
  
      def to_dict(self):
          return {"expr": repr(self.expr)}
  
      def _to_expr(self):
          return repr(self.expr)
  
      def _from_expr(self, expr):
          return SelectionExpression(expr=expr)
  
  
  def check_fields_and_encodings(parameter, field_name):
      for prop in ["fields", "encodings"]:
          try:
              if field_name in getattr(parameter.param.select, prop):
                  return True
          except (AttributeError, TypeError):
              pass
  
      return False
  
  
  # ------------------------------------------------------------------------
  # Top-Level Functions
  
  
  def value(value, **kwargs):
      """Specify a value for use in an encoding"""
      return dict(value=value, **kwargs)
  
  
  def parameter(name=None, select=None, **kwds):
      """Create a named parameter.
  
      Parameters
      ----------
      name : string (optional)
          The name of the parameter. If not specified, a unique name will be
          created.
      **kwds :
          additional keywords will be used to construct a parameter.  If 'select'
          is among the keywords, then a SelectionParameter will be created.
          Otherwise, a VariableParameter will be created.
  
      Returns
      -------
      parameter: Parameter
          The parameter object that can be used in chart creation.
      """
  
      parameter = Parameter(name)
  
      if "empty" in kwds:
          parameter.empty = kwds.pop("empty")
          if parameter.empty == "none":
              warnings.warn(
                  """The value of 'empty' should be True or False.""",
                  DeprecationWarning,
              )
              parameter.empty = False
          elif parameter.empty == "all":
              warnings.warn(
                  """The value of 'empty' should be True or False.""",
                  DeprecationWarning,
              )
              parameter.empty = True
          elif (parameter.empty is False) or (parameter.empty is True):
              pass
          else:
              raise ValueError("The value of 'empty' should be True or False.")
  
      if "init" in kwds:
          warnings.warn(
              """Use 'value' instead of 'init'.""",
              DeprecationWarning,
          )
          if "value" not in kwds:
              kwds["value"] = kwds.pop("init")
          else:
              # If both 'value' and 'init' are set, we ignore 'init'.
              kwds.pop("init")
  
      if select is None:
          parameter.param = core.VariableParameter(name=parameter.name, **kwds)
          parameter.param_type = "variable"
      else:
          parameter.param = core.SelectionParameter(
              name=parameter.name, select=select, **kwds
          )
          parameter.param_type = "selection"
  
      return parameter
  
  
  # TODO: Update the docstring
  def selection(type=Undefined, **kwds):
      """Create a selection.
  
      Parameters
      ----------
      type : string
          The type of the selection: either "interval" or "point"]
      **kwds :
          additional keywords to control the selection.
  
      Returns
      -------
      Parameter
          The Parameter object that can be used in chart creation.
      """
  
      # We separate out the parameter keywords from the selection keywords
      param_kwds = {}
  
      for kwd in {"name", "value", "bind", "empty", "init"}:
          if kwd in kwds:
              param_kwds[kwd] = kwds.pop(kwd)
  
      if type == "interval":
          select = core.IntervalSelectionConfig(type=type, **kwds)
      elif type == "point":
          select = core.PointSelectionConfig(type=type, **kwds)
      elif type in ["single", "multi"]:
          select = core.PointSelectionConfig(type="point", **kwds)
          warnings.warn(
              """The types 'single' and 'multi' are now
          combined and should be specified using "type='point'".""",
              DeprecationWarning,
          )
      else:
          raise ValueError("""'type' must be 'point' or 'interval'""")
  
      return parameter(select=select, **param_kwds)
  
  
  @utils.use_signature(core.IntervalSelectionConfig)
  def selection_interval(**kwargs):
      """Create a selection parameter with type='interval'"""
      return selection(type="interval", **kwargs)
  
  
  @utils.use_signature(core.PointSelectionConfig)
  def selection_point(**kwargs):
      """Create a selection with type='point'"""
      return selection(type="point", **kwargs)
  
  
  @utils.use_signature(core.PointSelectionConfig)
  def selection_multi(**kwargs):
      warnings.warn(
          """'selection_multi' is deprecated.  Use 'selection_point'""",
          DeprecationWarning,
      )
      return selection(type="point", **kwargs)
  
  
  @utils.use_signature(core.PointSelectionConfig)
  def selection_single(**kwargs):
      warnings.warn(
          """'selection_single' is deprecated.  Use 'selection_point'""",
          DeprecationWarning,
      )
      return selection(type="point", **kwargs)
  
  
  @utils.use_signature(core.Binding)
  def binding(input, **kwargs):
      """A generic binding"""
      return core.Binding(input=input, **kwargs)
  
  
  @utils.use_signature(core.BindCheckbox)
  def binding_checkbox(**kwargs):
      """A checkbox binding"""
      return core.BindCheckbox(input="checkbox", **kwargs)
  
  
  @utils.use_signature(core.BindRadioSelect)
  def binding_radio(**kwargs):
      """A radio button binding"""
      return core.BindRadioSelect(input="radio", **kwargs)
  
  
  @utils.use_signature(core.BindRadioSelect)
  def binding_select(**kwargs):
      """A select binding"""
      return core.BindRadioSelect(input="select", **kwargs)
  
  
  @utils.use_signature(core.BindRange)
  def binding_range(**kwargs):
      """A range binding"""
      return core.BindRange(input="range", **kwargs)
  
  
  # TODO: update the docstring
  def condition(predicate, if_true, if_false, **kwargs):
      """A conditional attribute or encoding
  
      Parameters
      ----------
      predicate: Selection, PredicateComposition, expr.Expression, dict, or string
          the selection predicate or test predicate for the condition.
          if a string is passed, it will be treated as a test operand.
      if_true:
          the spec or object to use if the selection predicate is true
      if_false:
          the spec or object to use if the selection predicate is false
      **kwargs:
          additional keyword args are added to the resulting dict
  
      Returns
      -------
      spec: dict or VegaLiteSchema
          the spec that describes the condition
      """
      test_predicates = (str, expr.Expression, core.PredicateComposition)
  
      if isinstance(predicate, Parameter):
          if predicate.param_type == "selection" or predicate.param.expr is Undefined:
              condition = {"param": predicate.name}
              if "empty" in kwargs:
                  condition["empty"] = kwargs.pop("empty")
              elif isinstance(predicate.empty, bool):
                  condition["empty"] = predicate.empty
          else:
              condition = {"test": predicate.param.expr}
      elif isinstance(predicate, test_predicates):
          condition = {"test": predicate}
      elif isinstance(predicate, dict):
          condition = predicate
      else:
          raise NotImplementedError(
              "condition predicate of type {}" "".format(type(predicate))
          )
  
      if isinstance(if_true, core.SchemaBase):
          # convert to dict for now; the from_dict call below will wrap this
          # dict in the appropriate schema
          if_true = if_true.to_dict()
      elif isinstance(if_true, str):
          if_true = {"shorthand": if_true}
          if_true.update(kwargs)
      condition.update(if_true)
  
      if isinstance(if_false, core.SchemaBase):
          # For the selection, the channel definitions all allow selections
          # already. So use this SchemaBase wrapper if possible.
          selection = if_false.copy()
          selection.condition = condition
      elif isinstance(if_false, str):
          selection = {"condition": condition, "shorthand": if_false}
          selection.update(kwargs)
      else:
          selection = dict(condition=condition, **if_false)
  
      return selection
  
  
  # --------------------------------------------------------------------
  # Top-level objects
  
  
  class TopLevelMixin(mixins.ConfigMethodMixin):
      """Mixin for top-level chart objects such as Chart, LayeredChart, etc."""
  
      _class_is_valid_at_instantiation = False
  
      def to_dict(self, *args, **kwargs):
          """Convert the chart to a dictionary suitable for JSON export"""
          # We make use of three context markers:
          # - 'data' points to the data that should be referenced for column type
          #   inference.
          # - 'top_level' is a boolean flag that is assumed to be true; if it's
          #   true then a "$schema" arg is added to the dict.
          # - 'datasets' is a dict of named datasets that should be inserted
          #   in the top-level object
  
          # note: not a deep copy because we want datasets and data arguments to
          # be passed by reference
          context = kwargs.get("context", {}).copy()
          context.setdefault("datasets", {})
          is_top_level = context.get("top_level", True)
  
          copy = self.copy(deep=False)
          original_data = getattr(copy, "data", Undefined)
          copy.data = _prepare_data(original_data, context)
  
          if original_data is not Undefined:
              context["data"] = original_data
  
          # remaining to_dict calls are not at top level
          context["top_level"] = False
          kwargs["context"] = context
  
          try:
              dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
          except jsonschema.ValidationError:
              dct = None
  
          # If we hit an error, then re-convert with validate='deep' to get
          # a more useful traceback. We don't do this by default because it's
          # much slower in the case that there are no errors.
          if dct is None:
              kwargs["validate"] = "deep"
              dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
  
          # TODO: following entries are added after validation. Should they be validated?
          if is_top_level:
              # since this is top-level we add $schema if it's missing
              if "$schema" not in dct:
                  dct["$schema"] = SCHEMA_URL
  
              # apply theme from theme registry
              the_theme = themes.get()
              dct = utils.update_nested(the_theme(), dct, copy=True)
  
              # update datasets
              if context["datasets"]:
                  dct.setdefault("datasets", {}).update(context["datasets"])
  
          return dct
  
      def to_html(
          self,
          base_url="https://cdn.jsdelivr.net/npm/",
          output_div="vis",
          embed_options=None,
          json_kwds=None,
          fullhtml=True,
          requirejs=False,
      ):
          return utils.spec_to_html(
              self.to_dict(),
              mode="vega-lite",
              vegalite_version=VEGALITE_VERSION,
              vegaembed_version=VEGAEMBED_VERSION,
              vega_version=VEGA_VERSION,
              base_url=base_url,
              output_div=output_div,
              embed_options=embed_options,
              json_kwds=json_kwds,
              fullhtml=fullhtml,
              requirejs=requirejs,
          )
  
      def save(
          self,
          fp,
          format=None,
          override_data_transformer=True,
          scale_factor=1.0,
          vegalite_version=VEGALITE_VERSION,
          vega_version=VEGA_VERSION,
          vegaembed_version=VEGAEMBED_VERSION,
          **kwargs,
      ):
          """Save a chart to file in a variety of formats
  
          Supported formats are json, html, png, svg, pdf; the last three require
          the altair_saver package to be installed.
  
          Parameters
          ----------
          fp : string filename or file-like object
              file in which to write the chart.
          format : string (optional)
              the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
              If not specified, the format will be determined from the filename.
          override_data_transformer : boolean (optional)
              If True (default), then the save action will be done with
              the MaxRowsError disabled. If False, then do not change the data
              transformer.
          scale_factor : float
              For svg or png formats, scale the image by this factor when saving.
              This can be used to control the size or resolution of the output.
              Default is 1.0
          **kwargs :
              Additional keyword arguments are passed to the output method
              associated with the specified format.
  
          """
          from ...utils.save import save
  
          kwds = dict(
              chart=self,
              fp=fp,
              format=format,
              scale_factor=scale_factor,
              vegalite_version=vegalite_version,
              vega_version=vega_version,
              vegaembed_version=vegaembed_version,
              **kwargs,
          )
  
          # By default we override the data transformer. This makes it so
          # that save() will succeed even for large datasets that would
          # normally trigger a MaxRowsError
          if override_data_transformer:
              with data_transformers.disable_max_rows():
                  result = save(**kwds)
          else:
              result = save(**kwds)
          return result
  
      # Fallback for when rendering fails; the full repr is too long to be
      # useful in nearly all cases.
      def __repr__(self):
          return "alt.{}(...)".format(self.__class__.__name__)
  
      # Layering and stacking
      def __add__(self, other):
          if not isinstance(other, TopLevelMixin):
              raise ValueError("Only Chart objects can be layered.")
          return layer(self, other)
  
      def __and__(self, other):
          if not isinstance(other, TopLevelMixin):
              raise ValueError("Only Chart objects can be concatenated.")
          return vconcat(self, other)
  
      def __or__(self, other):
          if not isinstance(other, TopLevelMixin):
              raise ValueError("Only Chart objects can be concatenated.")
          return hconcat(self, other)
  
      def repeat(
          self,
          repeat=Undefined,
          row=Undefined,
          column=Undefined,
          layer=Undefined,
          columns=Undefined,
          **kwargs,
      ):
          """Return a RepeatChart built from the chart
  
          Fields within the chart can be set to correspond to the row or
          column using `alt.repeat('row')` and `alt.repeat('column')`.
  
          Parameters
          ----------
          repeat : list
              a list of data column names to be repeated. This cannot be
              used along with the ``row``, ``column`` or ``layer`` argument.
          row : list
              a list of data column names to be mapped to the row facet
          column : list
              a list of data column names to be mapped to the column facet
          layer : list
              a list of data column names to be layered. This cannot be
              used along with the ``row``, ``column`` or ``repeat`` argument.
          columns : int
              the maximum number of columns before wrapping. Only referenced
              if ``repeat`` is specified.
          **kwargs :
              additional keywords passed to RepeatChart.
  
          Returns
          -------
          chart : RepeatChart
              a repeated chart.
          """
          repeat_specified = repeat is not Undefined
          rowcol_specified = row is not Undefined or column is not Undefined
          layer_specified = layer is not Undefined
  
          if repeat_specified and rowcol_specified:
              raise ValueError(
                  "repeat argument cannot be combined with row/column argument."
              )
          elif repeat_specified and layer_specified:
              raise ValueError("repeat argument cannot be combined with layer argument.")
          elif layer_specified and rowcol_specified:
              raise ValueError(
                  "layer argument cannot be combined with row/column argument."
              )
  
          if repeat_specified:
              repeat = repeat
          elif layer_specified:
              repeat = core.LayerRepeatMapping(layer=layer)
          else:
              repeat = core.RepeatMapping(row=row, column=column)
  
          return RepeatChart(spec=self, repeat=repeat, columns=columns, **kwargs)
  
      def properties(self, **kwargs):
          """Set top-level properties of the Chart.
  
          Argument names and types are the same as class initialization.
          """
          copy = self.copy(deep=False)
          for key, val in kwargs.items():
              if key == "selection" and isinstance(val, Parameter):
                  # TODO: Can this be removed
                  # For backward compatibility with old selection interface.
                  setattr(copy, key, {val.name: val.selection})
              else:
                  # Don't validate data, because it hasn't been processed.
                  if key != "data":
                      self.validate_property(key, val)
                  setattr(copy, key, val)
          return copy
  
      def project(
          self,
          type="mercator",
          center=Undefined,
          clipAngle=Undefined,
          clipExtent=Undefined,
          coefficient=Undefined,
          distance=Undefined,
          fraction=Undefined,
          lobes=Undefined,
          parallel=Undefined,
          precision=Undefined,
          radius=Undefined,
          ratio=Undefined,
          reflectX=Undefined,
          reflectY=Undefined,
          rotate=Undefined,
          scale=Undefined,
          spacing=Undefined,
          tilt=Undefined,
          translate=Undefined,
          **kwds,
      ):
          """Add a geographic projection to the chart.
  
          This is generally used either with ``mark_geoshape`` or with the
          ``latitude``/``longitude`` encodings.
  
          Available projection types are
          ['albers', 'albersUsa', 'azimuthalEqualArea', 'azimuthalEquidistant',
          'conicConformal', 'conicEqualArea', 'conicEquidistant', 'equalEarth', 'equirectangular',
          'gnomonic', 'identity', 'mercator', 'orthographic', 'stereographic', 'transverseMercator']
  
          Attributes
          ----------
          type : ProjectionType
              The cartographic projection to use. This value is case-insensitive, for example
              `"albers"` and `"Albers"` indicate the same projection type. You can find all valid
              projection types [in the
              documentation](https://vega.github.io/vega-lite/docs/projection.html#projection-types).
  
              **Default value:** `mercator`
          center : List(float)
              Sets the projection’s center to the specified center, a two-element array of
              longitude and latitude in degrees.
  
              **Default value:** `[0, 0]`
          clipAngle : float
              Sets the projection’s clipping circle radius to the specified angle in degrees. If
              `null`, switches to [antimeridian](http://bl.ocks.org/mbostock/3788999) cutting
              rather than small-circle clipping.
          clipExtent : List(List(float))
              Sets the projection’s viewport clip extent to the specified bounds in pixels. The
              extent bounds are specified as an array `[[x0, y0], [x1, y1]]`, where `x0` is the
              left-side of the viewport, `y0` is the top, `x1` is the right and `y1` is the
              bottom. If `null`, no viewport clipping is performed.
          coefficient : float
  
          distance : float
  
          fraction : float
  
          lobes : float
  
          parallel : float
  
          precision : Mapping(required=[length])
              Sets the threshold for the projection’s [adaptive
              resampling](http://bl.ocks.org/mbostock/3795544) to the specified value in pixels.
              This value corresponds to the [Douglas–Peucker
              distance](http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm).
               If precision is not specified, returns the projection’s current resampling
              precision which defaults to `√0.5 ≅ 0.70710…`.
          radius : float
  
          ratio : float
  
          reflectX : boolean
  
          reflectY : boolean
  
          rotate : List(float)
              Sets the projection’s three-axis rotation to the specified angles, which must be a
              two- or three-element array of numbers [`lambda`, `phi`, `gamma`] specifying the
              rotation angles in degrees about each spherical axis. (These correspond to yaw,
              pitch and roll.)
  
              **Default value:** `[0, 0, 0]`
          scale : float
              Sets the projection's scale (zoom) value, overriding automatic fitting.
  
          spacing : float
  
          tilt : float
  
          translate : List(float)
              Sets the projection's translation (pan) value, overriding automatic fitting.
  
          """
          projection = core.Projection(
              center=center,
              clipAngle=clipAngle,
              clipExtent=clipExtent,
              coefficient=coefficient,
              distance=distance,
              fraction=fraction,
              lobes=lobes,
              parallel=parallel,
              precision=precision,
              radius=radius,
              ratio=ratio,
              reflectX=reflectX,
              reflectY=reflectY,
              rotate=rotate,
              scale=scale,
              spacing=spacing,
              tilt=tilt,
              translate=translate,
              type=type,
              **kwds,
          )
          return self.properties(projection=projection)
  
      def _add_transform(self, *transforms):
          """Copy the chart and add specified transforms to chart.transform"""
          copy = self.copy(deep=["transform"])
          if copy.transform is Undefined:
              copy.transform = []
          copy.transform.extend(transforms)
          return copy
  
      def transform_aggregate(self, aggregate=Undefined, groupby=Undefined, **kwds):
          """
          Add an AggregateTransform to the schema.
  
          Parameters
          ----------
          aggregate : List(:class:`AggregatedFieldDef`)
              Array of objects that define fields to aggregate.
          groupby : List(string)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          **kwds :
              additional keywords are converted to aggregates using standard
              shorthand parsing.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          The aggregate transform allows you to specify transforms directly using
          the same shorthand syntax as used in encodings:
  
          >>> import altair as alt
          >>> chart1 = alt.Chart().transform_aggregate(
          ...     mean_acc='mean(Acceleration)',
          ...     groupby=['Origin']
          ... )
          >>> print(chart1.transform[0].to_json())  # doctest: +NORMALIZE_WHITESPACE
          {
            "aggregate": [
              {
                "as": "mean_acc",
                "field": "Acceleration",
                "op": "mean"
              }
            ],
            "groupby": [
              "Origin"
            ]
          }
  
          It also supports including AggregatedFieldDef instances or dicts directly,
          so you can create the above transform like this:
  
          >>> chart2 = alt.Chart().transform_aggregate(
          ...     [alt.AggregatedFieldDef(field='Acceleration', op='mean',
          ...                             **{'as': 'mean_acc'})],
          ...     groupby=['Origin']
          ... )
          >>> chart2.transform == chart1.transform
          True
  
          See Also
          --------
          alt.AggregateTransform : underlying transform object
  
          """
          if aggregate is Undefined:
              aggregate = []
          for key, val in kwds.items():
              parsed = utils.parse_shorthand(val)
              dct = {
                  "as": key,
                  "field": parsed.get("field", Undefined),
                  "op": parsed.get("aggregate", Undefined),
              }
              aggregate.append(core.AggregatedFieldDef(**dct))
          return self._add_transform(
              core.AggregateTransform(aggregate=aggregate, groupby=groupby)
          )
  
      def transform_bin(self, as_=Undefined, field=Undefined, bin=True, **kwargs):
          """
          Add a BinTransform to the schema.
  
          Parameters
          ----------
          as_ : anyOf(string, List(string))
              The output fields at which to write the start and end bin values.
          bin : anyOf(boolean, :class:`BinParams`)
              An object indicating bin properties, or simply ``true`` for using default bin
              parameters.
          field : string
              The data field to bin.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> chart = alt.Chart().transform_bin("x_binned", "x")
          >>> chart.transform[0]
          BinTransform({
            as: 'x_binned',
            bin: True,
            field: 'x'
          })
  
          >>> chart = alt.Chart().transform_bin("x_binned", "x",
          ...                                   bin=alt.Bin(maxbins=10))
          >>> chart.transform[0]
          BinTransform({
            as: 'x_binned',
            bin: BinParams({
              maxbins: 10
            }),
            field: 'x'
          })
  
          See Also
          --------
          alt.BinTransform : underlying transform object
  
          """
          if as_ is not Undefined:
              if "as" in kwargs:
                  raise ValueError(
                      "transform_bin: both 'as_' and 'as' passed as arguments."
                  )
              kwargs["as"] = as_
          kwargs["bin"] = bin
          kwargs["field"] = field
          return self._add_transform(core.BinTransform(**kwargs))
  
      def transform_calculate(self, as_=Undefined, calculate=Undefined, **kwargs):
          """
          Add a CalculateTransform to the schema.
  
          Parameters
          ----------
          as_ : string
              The field for storing the computed formula value.
          calculate : string or alt.expr expression
              A `expression <https://vega.github.io/vega-lite/docs/types.html#expression>`__
              string. Use the variable ``datum`` to refer to the current data object.
          **kwargs
              transforms can also be passed by keyword argument; see Examples
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> from altair import datum, expr
  
          >>> chart = alt.Chart().transform_calculate(y = 2 * expr.sin(datum.x))
          >>> chart.transform[0]
          CalculateTransform({
            as: 'y',
            calculate: (2 * sin(datum.x))
          })
  
          It's also possible to pass the ``CalculateTransform`` arguments directly:
  
          >>> kwds = {'as': 'y', 'calculate': '2 * sin(datum.x)'}
          >>> chart = alt.Chart().transform_calculate(**kwds)
          >>> chart.transform[0]
          CalculateTransform({
            as: 'y',
            calculate: '2 * sin(datum.x)'
          })
  
          As the first form is easier to write and understand, that is the
          recommended method.
  
          See Also
          --------
          alt.CalculateTransform : underlying transform object
          """
          if as_ is Undefined:
              as_ = kwargs.pop("as", Undefined)
          elif "as" in kwargs:
              raise ValueError(
                  "transform_calculate: both 'as_' and 'as' passed as arguments."
              )
          if as_ is not Undefined or calculate is not Undefined:
              dct = {"as": as_, "calculate": calculate}
              self = self._add_transform(core.CalculateTransform(**dct))
          for as_, calculate in kwargs.items():
              dct = {"as": as_, "calculate": calculate}
              self = self._add_transform(core.CalculateTransform(**dct))
          return self
  
      def transform_density(
          self,
          density,
          as_=Undefined,
          bandwidth=Undefined,
          counts=Undefined,
          cumulative=Undefined,
          extent=Undefined,
          groupby=Undefined,
          maxsteps=Undefined,
          minsteps=Undefined,
          steps=Undefined,
      ):
          """Add a DensityTransform to the spec.
  
          Attributes
          ----------
          density : str
              The data field for which to perform density estimation.
          as_ : [str, str]
              The output fields for the sample value and corresponding density estimate.
              **Default value:** ``["value", "density"]``
          bandwidth : float
              The bandwidth (standard deviation) of the Gaussian kernel. If unspecified or set to
              zero, the bandwidth value is automatically estimated from the input data using
              Scott’s rule.
          counts : boolean
              A boolean flag indicating if the output values should be probability estimates
              (false) or smoothed counts (true).
              **Default value:** ``false``
          cumulative : boolean
              A boolean flag indicating whether to produce density estimates (false) or cumulative
              density estimates (true).
              **Default value:** ``false``
          extent : List([float, float])
              A [min, max] domain from which to sample the distribution. If unspecified, the
              extent will be determined by the observed minimum and maximum values of the density
              value field.
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          maxsteps : float
              The maximum number of samples to take along the extent domain for plotting the
              density. **Default value:** ``200``
          minsteps : float
              The minimum number of samples to take along the extent domain for plotting the
              density. **Default value:** ``25``
          steps : float
              The exact number of samples to take along the extent domain for plotting the
              density. If specified, overrides both minsteps and maxsteps to set an exact number
              of uniform samples. Potentially useful in conjunction with a fixed extent to ensure
              consistent sample points for stacked densities.
          """
          return self._add_transform(
              core.DensityTransform(
                  density=density,
                  bandwidth=bandwidth,
                  counts=counts,
                  cumulative=cumulative,
                  extent=extent,
                  groupby=groupby,
                  maxsteps=maxsteps,
                  minsteps=minsteps,
                  steps=steps,
                  **{"as": as_},
              )
          )
  
      def transform_impute(
          self,
          impute,
          key,
          frame=Undefined,
          groupby=Undefined,
          keyvals=Undefined,
          method=Undefined,
          value=Undefined,
      ):
          """
          Add an ImputeTransform to the schema.
  
          Parameters
          ----------
          impute : string
              The data field for which the missing values should be imputed.
          key : string
              A key field that uniquely identifies data objects within a group.
              Missing key values (those occurring in the data but not in the current group) will
              be imputed.
          frame : List(anyOf(None, float))
              A frame specification as a two-element array used to control the window over which
              the specified method is applied. The array entries should either be a number
              indicating the offset from the current data object, or null to indicate unbounded
              rows preceding or following the current data object.  For example, the value ``[-5,
              5]`` indicates that the window should include five objects preceding and five
              objects following the current object.
              **Default value:** :  ``[null, null]`` indicating that the window includes all
              objects.
          groupby : List(string)
              An optional array of fields by which to group the values.
              Imputation will then be performed on a per-group basis.
          keyvals : anyOf(List(Mapping(required=[])), :class:`ImputeSequence`)
              Defines the key values that should be considered for imputation.
              An array of key values or an object defining a `number sequence
              <https://vega.github.io/vega-lite/docs/impute.html#sequence-def>`__.
              If provided, this will be used in addition to the key values observed within the
              input data.  If not provided, the values will be derived from all unique values of
              the ``key`` field. For ``impute`` in ``encoding``, the key field is the x-field if
              the y-field is imputed, or vice versa.
              If there is no impute grouping, this property *must* be specified.
          method : :class:`ImputeMethod`
              The imputation method to use for the field value of imputed data objects.
              One of ``value``, ``mean``, ``median``, ``max`` or ``min``.
              **Default value:**  ``"value"``
          value : Mapping(required=[])
              The field value to use when the imputation ``method`` is ``"value"``.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.ImputeTransform : underlying transform object
          """
          return self._add_transform(
              core.ImputeTransform(
                  impute=impute,
                  key=key,
                  frame=frame,
                  groupby=groupby,
                  keyvals=keyvals,
                  method=method,
                  value=value,
              )
          )
  
      def transform_joinaggregate(
          self, joinaggregate=Undefined, groupby=Undefined, **kwargs
      ):
          """
          Add a JoinAggregateTransform to the schema.
  
          Parameters
          ----------
          joinaggregate : List(:class:`JoinAggregateFieldDef`)
              The definition of the fields in the join aggregate, and what calculations to use.
          groupby : List(string)
              The data fields for partitioning the data objects into separate groups. If
              unspecified, all data points will be in a single group.
          **kwargs
              joinaggregates can also be passed by keyword argument; see Examples.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> chart = alt.Chart().transform_joinaggregate(x='sum(y)')
          >>> chart.transform[0]
          JoinAggregateTransform({
            joinaggregate: [JoinAggregateFieldDef({
              as: 'x',
              field: 'y',
              op: 'sum'
            })]
          })
  
          See Also
          --------
          alt.JoinAggregateTransform : underlying transform object
          """
          if joinaggregate is Undefined:
              joinaggregate = []
          for key, val in kwargs.items():
              parsed = utils.parse_shorthand(val)
              dct = {
                  "as": key,
                  "field": parsed.get("field", Undefined),
                  "op": parsed.get("aggregate", Undefined),
              }
              joinaggregate.append(core.JoinAggregateFieldDef(**dct))
          return self._add_transform(
              core.JoinAggregateTransform(joinaggregate=joinaggregate, groupby=groupby)
          )
  
      # TODO: Update docstring
      def transform_filter(self, filter, **kwargs):
          """
          Add a FilterTransform to the schema.
  
          Parameters
          ----------
          filter : a filter expression or :class:`PredicateComposition`
              The `filter` property must be one of the predicate definitions:
              (1) a string or alt.expr expression
              (2) a range predicate
              (3) a selection predicate
              (4) a logical operand combining (1)-(3)
              (5) a Selection object
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.FilterTransform : underlying transform object
  
          """
          if isinstance(filter, Parameter):
              new_filter = {"param": filter.name}
              if "empty" in kwargs:
                  new_filter["empty"] = kwargs.pop("empty")
              elif isinstance(filter.empty, bool):
                  new_filter["empty"] = filter.empty
              filter = new_filter
          return self._add_transform(core.FilterTransform(filter=filter, **kwargs))
  
      def transform_flatten(self, flatten, as_=Undefined):
          """Add a FlattenTransform to the schema.
  
          Parameters
          ----------
          flatten : List(string)
              An array of one or more data fields containing arrays to flatten.
              If multiple fields are specified, their array values should have a parallel
              structure, ideally with the same length.
              If the lengths of parallel arrays do not match,
              the longest array will be used with ``null`` values added for missing entries.
          as : List(string)
              The output field names for extracted array values.
              **Default value:** The field name of the corresponding array field
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.FlattenTransform : underlying transform object
          """
          return self._add_transform(
              core.FlattenTransform(flatten=flatten, **{"as": as_})
          )
  
      def transform_fold(self, fold, as_=Undefined):
          """Add a FoldTransform to the spec.
  
          Parameters
          ----------
          fold : List(string)
              An array of data fields indicating the properties to fold.
          as : [string, string]
              The output field names for the key and value properties produced by the fold
              transform. Default: ``["key", "value"]``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_pivot : pivot transform - opposite of fold.
          alt.FoldTransform : underlying transform object
          """
          return self._add_transform(core.FoldTransform(fold=fold, **{"as": as_}))
  
      def transform_loess(
          self, on, loess, as_=Undefined, bandwidth=Undefined, groupby=Undefined
      ):
          """Add a LoessTransform to the spec.
  
          Parameters
          ----------
          on : str
              The data field of the independent variable to use a predictor.
          loess : str
              The data field of the dependent variable to smooth.
          as_ : [str, str]
              The output field names for the smoothed points generated by the loess transform.
              **Default value:** The field names of the input x and y values.
          bandwidth : float
              A bandwidth parameter in the range ``[0, 1]`` that determines the amount of
              smoothing. **Default value:** ``0.3``
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_regression: regression transform
          alt.LoessTransform : underlying transform object
          """
          return self._add_transform(
              core.LoessTransform(
                  loess=loess, on=on, bandwidth=bandwidth, groupby=groupby, **{"as": as_}
              )
          )
  
      def transform_lookup(
          self,
          lookup=Undefined,
          from_=Undefined,
          as_=Undefined,
          default=Undefined,
          **kwargs,
      ):
          """Add a DataLookupTransform or SelectionLookupTransform to the chart
  
          Attributes
          ----------
          lookup : string
              Key in primary data source.
          from_ : anyOf(:class:`LookupData`, :class:`LookupSelection`)
              Secondary data reference.
          as_ : anyOf(string, List(string))
              The output fields on which to store the looked up data values.
  
              For data lookups, this property may be left blank if ``from_.fields``
              has been specified (those field names will be used); if ``from_.fields``
              has not been specified, ``as_`` must be a string.
  
              For selection lookups, this property is optional: if unspecified,
              looked up values will be stored under a property named for the selection;
              and if specified, it must correspond to ``from_.fields``.
          default : string
              The default value to use if lookup fails. **Default value:** ``null``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.DataLookupTransform : underlying transform object
          alt.SelectionLookupTransform : underlying transform object
          """
          if as_ is not Undefined:
              if "as" in kwargs:
                  raise ValueError(
                      "transform_lookup: both 'as_' and 'as' passed as arguments."
                  )
              kwargs["as"] = as_
          if from_ is not Undefined:
              if "from" in kwargs:
                  raise ValueError(
                      "transform_lookup: both 'from_' and 'from' passed as arguments."
                  )
              kwargs["from"] = from_
          kwargs["lookup"] = lookup
          kwargs["default"] = default
          return self._add_transform(core.LookupTransform(**kwargs))
  
      def transform_pivot(
          self, pivot, value, groupby=Undefined, limit=Undefined, op=Undefined
      ):
          """Add a pivot transform to the chart.
  
          Parameters
          ----------
          pivot : str
              The data field to pivot on. The unique values of this field become new field names
              in the output stream.
          value : str
              The data field to populate pivoted fields. The aggregate values of this field become
              the values of the new pivoted fields.
          groupby : List(str)
              The optional data fields to group by. If not specified, a single group containing
              all data objects will be used.
          limit : float
              An optional parameter indicating the maximum number of pivoted fields to generate.
              The default ( ``0`` ) applies no limit. The pivoted ``pivot`` names are sorted in
              ascending order prior to enforcing the limit.
              **Default value:** ``0``
          op : string
              The aggregation operation to apply to grouped ``value`` field values.
              **Default value:** ``sum``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_fold : fold transform - opposite of pivot.
          alt.PivotTransform : underlying transform object
          """
          return self._add_transform(
              core.PivotTransform(
                  pivot=pivot, value=value, groupby=groupby, limit=limit, op=op
              )
          )
  
      def transform_quantile(
          self,
          quantile,
          as_=Undefined,
          groupby=Undefined,
          probs=Undefined,
          step=Undefined,
      ):
          """Add a quantile transform to the chart
  
          Parameters
          ----------
          quantile : str
              The data field for which to perform quantile estimation.
          as : [str, str]
              The output field names for the probability and quantile values.
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          probs : List(float)
              An array of probabilities in the range (0, 1) for which to compute quantile values.
              If not specified, the *step* parameter will be used.
          step : float
              A probability step size (default 0.01) 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 *probs* parameter is not provided. **Default value:** ``["prob", "value"]``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.QuantileTransform : underlying transform object
          """
          return self._add_transform(
              core.QuantileTransform(
                  quantile=quantile,
                  groupby=groupby,
                  probs=probs,
                  step=step,
                  **{"as": as_},
              )
          )
  
      def transform_regression(
          self,
          on,
          regression,
          as_=Undefined,
          extent=Undefined,
          groupby=Undefined,
          method=Undefined,
          order=Undefined,
          params=Undefined,
      ):
          """Add a RegressionTransform to the chart.
  
          Parameters
          ----------
          on : str
              The data field of the independent variable to use a predictor.
          regression : str
              The data field of the dependent variable to predict.
          as_ : [str, str]
              The output field names for the smoothed points generated by the regression
              transform. **Default value:** The field names of the input x and y values.
          extent : [float, float]
              A [min, max] domain over the independent (x) field for the starting and ending
              points of the generated trend line.
          groupby : List(str)
              The data fields to group by. If not specified, a single group containing all data
              objects will be used.
          method : enum('linear', 'log', 'exp', 'pow', 'quad', 'poly')
              The functional form of the regression model. One of ``"linear"``, ``"log"``,
              ``"exp"``, ``"pow"``, ``"quad"``, or ``"poly"``.  **Default value:** ``"linear"``
          order : float
              The polynomial order (number of coefficients) for the 'poly' method.
              **Default value:** ``3``
          params : boolean
              A boolean flag indicating if the transform should return the regression model
              parameters (one object per group), rather than trend line points.
              The resulting objects include a ``coef`` array of fitted coefficient values
              (starting with the intercept term and then including terms of increasing order)
              and an ``rSquared`` value (indicating the total variance explained by the model).
              **Default value:** ``false``
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          Chart.transform_loess : LOESS transform
          alt.RegressionTransform : underlying transform object
          """
          return self._add_transform(
              core.RegressionTransform(
                  regression=regression,
                  on=on,
                  extent=extent,
                  groupby=groupby,
                  method=method,
                  order=order,
                  params=params,
                  **{"as": as_},
              )
          )
  
      def transform_sample(self, sample=1000):
          """
          Add a SampleTransform to the schema.
  
          Parameters
          ----------
          sample : float
              The maximum number of data objects to include in the sample. Default: 1000.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.SampleTransform : underlying transform object
          """
          return self._add_transform(core.SampleTransform(sample))
  
      def transform_stack(self, as_, stack, groupby, offset=Undefined, sort=Undefined):
          """
          Add a StackTransform to the schema.
  
          Parameters
          ----------
          as_ : anyOf(string, List(string))
              Output field names. This can be either a string or an array of strings with
              two elements denoting the name for the fields for stack start and stack end
              respectively.
              If a single string(eg."val") is provided, the end field will be "val_end".
          stack : string
              The field which is stacked.
          groupby : List(string)
              The data fields to group by.
          offset : enum('zero', 'center', 'normalize')
              Mode for stacking marks. Default: 'zero'.
          sort : List(:class:`SortField`)
              Field that determines the order of leaves in the stacked charts.
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          See Also
          --------
          alt.StackTransform : underlying transform object
          """
          return self._add_transform(
              core.StackTransform(
                  stack=stack, groupby=groupby, offset=offset, sort=sort, **{"as": as_}
              )
          )
  
      def transform_timeunit(
          self, as_=Undefined, field=Undefined, timeUnit=Undefined, **kwargs
      ):
          """
          Add a TimeUnitTransform to the schema.
  
          Parameters
          ----------
          as_ : string
              The output field to write the timeUnit value.
          field : string
              The data field to apply time unit.
          timeUnit : :class:`TimeUnit`
              The timeUnit.
          **kwargs
              transforms can also be passed by keyword argument; see Examples
  
          Returns
          -------
          self : Chart object
              returns chart to allow for chaining
  
          Examples
          --------
          >>> import altair as alt
          >>> from altair import datum, expr
  
          >>> chart = alt.Chart().transform_timeunit(month='month(date)')
          >>> chart.transform[0]
          TimeUnitTransform({
            as: 'month',
            field: 'date',
            timeUnit: 'month'
          })
  
          It's also possible to pass the ``TimeUnitTransform`` arguments directly;
          this is most useful in cases where the desired field name is not a
          valid python identifier:
  
          >>> kwds = {'as': 'month', 'timeUnit': 'month', 'field': 'The Month'}
          >>> chart = alt.Chart().transform_timeunit(**kwds)
          >>> chart.transform[0]
          TimeUnitTransform({
            as: 'month',
            field: 'The Month',
            timeUnit: 'month'
          })
  
          As the first form is easier to write and understand, that is the
          recommended method.
  
          See Also
          --------
          alt.TimeUnitTransform : underlying transform object
  
          """
          if as_ is Undefined:
              as_ = kwargs.pop("as", Undefined)
          else:
              if "as" in kwargs:
                  raise ValueError(
                      "transform_timeunit: both 'as_' and 'as' passed as arguments."
                  )
          if as_ is not Undefined:
              dct = {"as": as_, "timeUnit": timeUnit, "field": field}
              self = self._add_transform(core.TimeUnitTransform(**dct))
          for as_, shorthand in kwargs.items():
              dct = utils.parse_shorthand(
                  shorthand,
                  parse_timeunits=True,
                  parse_aggregates=False,
                  parse_types=False,
              )
              dct.pop("type", None)
              dct["as"] = as_
              if "timeUnit" not in dct:
                  raise ValueError("'{}' must include a valid timeUnit".format(shorthand))
              self = self._add_transform(core.TimeUnitTransform(**dct))
          return self
  
      def transform_window(
          self,
          window=Undefined,
          frame=Undefined,
          groupby=Undefined,
          ignorePeers=Undefined,
          sort=Undefined,
          **kwargs,
      ):
          """Add a WindowTransform to the schema
  
          Parameters
          ----------
          window : List(:class:`WindowFieldDef`)
              The definition of the fields in the window, and what calculations to use.
          frame : List(anyOf(None, float))
              A frame specification as a two-element array indicating how the sliding window
              should proceed. The array entries should either be a number indicating the offset
              from the current data object, or null to indicate unbounded rows preceding or
              following the current data object. The default value is ``[null, 0]``, indicating
              that the sliding window includes the current object and all preceding objects. The
              value ``[-5, 5]`` indicates that the window should include five objects preceding
              and five objects following the current object. Finally, ``[null, null]`` indicates
              that the window frame should always include all data objects. The only operators
              affected are the aggregation operations and the ``first_value``, ``last_value``, and
              ``nth_value`` window operations. The other window operations are not affected by
              this.
  
              **Default value:** :  ``[null, 0]`` (includes the current object and all preceding
              objects)
          groupby : List(string)
              The data fields for partitioning the data objects into separate windows. If
              unspecified, all data points will be in a single group.
          ignorePeers : boolean
              Indicates if the sliding window frame should ignore peer values. (Peer values are
              those considered identical by the sort criteria). The default is false, causing the
              window frame to expand to include all peer values. If set to true, the window frame
              will be defined by offset values only. This setting only affects those operations
              that depend on the window frame, namely aggregation operations and the first_value,
              last_value, and nth_value window operations.
  
              **Default value:** ``false``
          sort : List(:class:`SortField`)
              A sort field definition for sorting data objects within a window. If two data
              objects are considered equal by the comparator, they are considered “peer” values of
              equal rank. If sort is not specified, the order is undefined: data objects are
              processed in the order they are observed and none are considered peers (the
              ignorePeers parameter is ignored and treated as if set to ``true`` ).
          **kwargs
              transforms can also be passed by keyword argument; see Examples
  
          Examples
          --------
          A cumulative line chart
  
          >>> import altair as alt
          >>> import numpy as np
          >>> import pandas as pd
          >>> data = pd.DataFrame({'x': np.arange(100),
          ...                      'y': np.random.randn(100)})
          >>> chart = alt.Chart(data).mark_line().encode(
          ...     x='x:Q',
          ...     y='ycuml:Q'
          ... ).transform_window(
          ...     ycuml='sum(y)'
          ... )
          >>> chart.transform[0]
          WindowTransform({
            window: [WindowFieldDef({
              as: 'ycuml',
              field: 'y',
              op: 'sum'
            })]
          })
  
          """
          if kwargs:
              if window is Undefined:
                  window = []
              for as_, shorthand in kwargs.items():
                  kwds = {"as": as_}
                  kwds.update(
                      utils.parse_shorthand(
                          shorthand,
                          parse_aggregates=False,
                          parse_window_ops=True,
                          parse_timeunits=False,
                          parse_types=False,
                      )
                  )
                  window.append(core.WindowFieldDef(**kwds))
  
          return self._add_transform(
              core.WindowTransform(
                  window=window,
                  frame=frame,
                  groupby=groupby,
                  ignorePeers=ignorePeers,
                  sort=sort,
              )
          )
  
      # Display-related methods
  
      def _repr_mimebundle_(self, include=None, exclude=None):
          """Return a MIME bundle for display in Jupyter frontends."""
          # Catch errors explicitly to get around issues in Jupyter frontend
          # see https://github.com/ipython/ipython/issues/11038
          try:
              dct = self.to_dict()
          except Exception:
              utils.display_traceback(in_ipython=True)
              return {}
          else:
              return renderers.get()(dct)
  
      def display(self, renderer=Undefined, theme=Undefined, actions=Undefined, **kwargs):
          """Display chart in Jupyter notebook or JupyterLab
  
          Parameters are passed as options to vega-embed within supported frontends.
          See https://github.com/vega/vega-embed#options for details.
  
          Parameters
          ----------
          renderer : string ('canvas' or 'svg')
              The renderer to use
          theme : string
              The Vega theme name to use; see https://github.com/vega/vega-themes
          actions : bool or dict
              Specify whether action links ("Open In Vega Editor", etc.) are
              included in the view.
          **kwargs :
              Additional parameters are also passed to vega-embed as options.
  
          """
          from IPython.display import display
  
          if renderer is not Undefined:
              kwargs["renderer"] = renderer
          if theme is not Undefined:
              kwargs["theme"] = theme
          if actions is not Undefined:
              kwargs["actions"] = actions
  
          if kwargs:
              options = renderers.options.copy()
              options["embed_options"] = options.get("embed_options", {}).copy()
              options["embed_options"].update(kwargs)
              with renderers.enable(**options):
                  display(self)
          else:
              display(self)
  
      @utils.deprecation.deprecated(message="serve() is deprecated. Use show() instead.")
      def serve(
          self,
          ip="127.0.0.1",
          port=8888,
          n_retries=50,
          files=None,
          jupyter_warning=True,
          open_browser=True,
          http_server=None,
          **kwargs,
      ):
          """Open a browser window and display a rendering of the chart
  
          Parameters
          ----------
          html : string
              HTML to serve
          ip : string (default = '127.0.0.1')
              ip address at which the HTML will be served.
          port : int (default = 8888)
              the port at which to serve the HTML
          n_retries : int (default = 50)
              the number of nearby ports to search if the specified port
              is already in use.
          files : dictionary (optional)
              dictionary of extra content to serve
          jupyter_warning : bool (optional)
              if True (default), then print a warning if this is used
              within the Jupyter notebook
          open_browser : bool (optional)
              if True (default), then open a web browser to the given HTML
          http_server : class (optional)
              optionally specify an HTTPServer class to use for showing the
              figure. The default is Python's basic HTTPServer.
          **kwargs :
              additional keyword arguments passed to the save() method
  
          """
          from ...utils.server import serve
  
          html = io.StringIO()
          self.save(html, format="html", **kwargs)
          html.seek(0)
  
          serve(
              html.read(),
              ip=ip,
              port=port,
              n_retries=n_retries,
              files=files,
              jupyter_warning=jupyter_warning,
              open_browser=open_browser,
              http_server=http_server,
          )
  
      def show(self, embed_opt=None, open_browser=None):
          """Show the chart in an external browser window.
  
          This requires a recent version of the altair_viewer package.
  
          Parameters
          ----------
          embed_opt : dict (optional)
              The Vega embed options that control the dispay of the chart.
          open_browser : bool (optional)
              Specify whether a browser window should be opened. If not specified,
              a browser window will be opened only if the server is not already
              connected to a browser.
          """
          try:
              import altair_viewer  # type: ignore
          except ImportError:
              raise ValueError(
                  "show() method requires the altair_viewer package. "
                  "See http://github.com/altair-viz/altair_viewer"
              )
          altair_viewer.show(self, embed_opt=embed_opt, open_browser=open_browser)
  
      @utils.use_signature(core.Resolve)
      def _set_resolve(self, **kwargs):
          """Copy the chart and update the resolve property with kwargs"""
          if not hasattr(self, "resolve"):
              raise ValueError(
                  "{} object has no attribute " "'resolve'".format(self.__class__)
              )
          copy = self.copy(deep=["resolve"])
          if copy.resolve is Undefined:
              copy.resolve = core.Resolve()
          for key, val in kwargs.items():
              copy.resolve[key] = val
          return copy
  
      @utils.use_signature(core.AxisResolveMap)
      def resolve_axis(self, *args, **kwargs):
          return self._set_resolve(axis=core.AxisResolveMap(*args, **kwargs))
  
      @utils.use_signature(core.LegendResolveMap)
      def resolve_legend(self, *args, **kwargs):
          return self._set_resolve(legend=core.LegendResolveMap(*args, **kwargs))
  
      @utils.use_signature(core.ScaleResolveMap)
      def resolve_scale(self, *args, **kwargs):
          return self._set_resolve(scale=core.ScaleResolveMap(*args, **kwargs))
  
  
  class _EncodingMixin(object):
      @utils.use_signature(core.FacetedEncoding)
      def encode(self, *args, **kwargs):
          # Convert args to kwargs based on their types.
          kwargs = utils.infer_encoding_types(args, kwargs, channels)
  
          # get a copy of the dict representation of the previous encoding
          copy = self.copy(deep=["encoding"])
          encoding = copy._get("encoding", {})
          if isinstance(encoding, core.VegaLiteSchema):
              encoding = {k: v for k, v in encoding._kwds.items() if v is not Undefined}
  
          # update with the new encodings, and apply them to the copy
          encoding.update(kwargs)
          copy.encoding = core.FacetedEncoding(**encoding)
          return copy
  
      def facet(
          self,
          facet=Undefined,
          row=Undefined,
          column=Undefined,
          data=Undefined,
          columns=Undefined,
          **kwargs,
      ):
          """Create a facet chart from the current chart.
  
          Faceted charts require data to be specified at the top level; if data
          is not specified, the data from the current chart will be used at the
          top level.
  
          Parameters
          ----------
          facet : string or alt.Facet (optional)
              The data column to use as an encoding for a wrapped facet.
              If specified, then neither row nor column may be specified.
          column : string or alt.Column (optional)
              The data column to use as an encoding for a column facet.
              May be combined with row argument, but not with facet argument.
          row : string or alt.Column (optional)
              The data column to use as an encoding for a row facet.
              May be combined with column argument, but not with facet argument.
          data : string or dataframe (optional)
              The dataset to use for faceting. If not supplied, then data must
              be specified in the top-level chart that calls this method.
          columns : integer
              the maximum number of columns for a wrapped facet.
  
          Returns
          -------
          self :
              for chaining
          """
          facet_specified = facet is not Undefined
          rowcol_specified = row is not Undefined or column is not Undefined
  
          if facet_specified and rowcol_specified:
              raise ValueError(
                  "facet argument cannot be combined with row/column argument."
              )
  
          if data is Undefined:
              if self.data is Undefined:
                  raise ValueError(
                      "Facet charts require data to be specified at the top level."
                  )
              self = self.copy(deep=False)
              data, self.data = self.data, Undefined
  
          if facet_specified:
              if isinstance(facet, str):
                  facet = channels.Facet(facet)
          else:
              facet = FacetMapping(row=row, column=column)
  
          return FacetChart(spec=self, facet=facet, data=data, columns=columns, **kwargs)
  
  
  class Chart(
      TopLevelMixin, _EncodingMixin, mixins.MarkMethodMixin, core.TopLevelUnitSpec
  ):
      """Create a basic Altair/Vega-Lite chart.
  
      Although it is possible to set all Chart properties as constructor attributes,
      it is more idiomatic to use methods such as ``mark_point()``, ``encode()``,
      ``transform_filter()``, ``properties()``, etc. See Altair's documentation
      for details and examples: http://altair-viz.github.io/.
  
      Attributes
      ----------
      data : Data
          An object describing the data source
      mark : AnyMark
          A string describing the mark type (one of `"bar"`, `"circle"`, `"square"`, `"tick"`,
           `"line"`, * `"area"`, `"point"`, `"rule"`, `"geoshape"`, and `"text"`) or a
           MarkDef object.
      encoding : FacetedEncoding
          A key-value mapping between encoding channels and definition of fields.
      autosize : anyOf(AutosizeType, AutoSizeParams)
          Sets how the visualization size should be determined. If a string, should be one of
          `"pad"`, `"fit"` or `"none"`. Object values can additionally specify parameters for
          content sizing and automatic resizing. `"fit"` is only supported for single and
          layered views that don't use `rangeStep`.  __Default value__: `pad`
      background : string
          CSS color property to use as the background of visualization.
  
          **Default value:** none (transparent)
      config : Config
          Vega-Lite configuration object.  This property can only be defined at the top-level
          of a specification.
      description : string
          Description of this mark for commenting purpose.
      height : float
          The height of a visualization.
      name : string
          Name of the visualization for later reference.
      padding : Padding
          The default visualization padding, in pixels, from the edge of the visualization
          canvas to the data rectangle.  If a number, specifies padding for all sides. If an
          object, the value should have the format `{"left": 5, "top": 5, "right": 5,
          "bottom": 5}` to specify padding for each side of the visualization.  __Default
          value__: `5`
      projection : Projection
          An object defining properties of geographic projection.  Works with `"geoshape"`
          marks and `"point"` or `"line"` marks that have a channel (one or more of `"X"`,
          `"X2"`, `"Y"`, `"Y2"`) with type `"latitude"`, or `"longitude"`.
      selection : Mapping(required=[])
          A key-value mapping between selection names and definitions.
      title : anyOf(string, TitleParams)
          Title for the plot.
      transform : List(Transform)
          An array of data transformations such as filter and new field calculation.
      width : float
          The width of a visualization.
      """
  
      def __init__(
          self,
          data=Undefined,
          encoding=Undefined,
          mark=Undefined,
          width=Undefined,
          height=Undefined,
          **kwargs,
      ):
          super(Chart, self).__init__(
              data=data,
              encoding=encoding,
              mark=mark,
              width=width,
              height=height,
              **kwargs,
          )
  
      @classmethod
      def from_dict(cls, dct, validate=True):
          """Construct class from a dictionary representation
  
          Parameters
          ----------
          dct : dictionary
              The dict from which to construct the class
          validate : boolean
              If True (default), then validate the input against the schema.
  
          Returns
          -------
          obj : Chart object
              The wrapped schema
  
          Raises
          ------
          jsonschema.ValidationError :
              if validate=True and dct does not conform to the schema
          """
          for class_ in TopLevelMixin.__subclasses__():
              if class_ is Chart:
                  class_ = super(Chart, cls)
              try:
                  return class_.from_dict(dct, validate=validate)
              except jsonschema.ValidationError:
                  pass
  
          # As a last resort, try using the Root vegalite object
          return core.Root.from_dict(dct, validate)
  
      def to_dict(self, *args, **kwargs):
          """Convert the chart to a dictionary suitable for JSON export."""
          context = kwargs.get("context", {})
          if self.data is Undefined and "data" not in context:
              # No data specified here or in parent: inject empty data
              # for easier specification of datum encodings.
              copy = self.copy(deep=False)
              copy.data = core.InlineData(values=[{}])
              return super(Chart, copy).to_dict(*args, **kwargs)
          return super().to_dict(*args, **kwargs)
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *params):
          warnings.warn(
              """'add_selection' is deprecated. Use 'add_parameter'.""",
              DeprecationWarning,
          )
          return self.add_parameter(*params)
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          encodings = []
          if bind_x:
              encodings.append("x")
          if bind_y:
              encodings.append("y")
          return self.add_selection(
              selection_interval(bind="scales", encodings=encodings)
          )
  
  
  def _check_if_valid_subspec(spec, classname):
      """Check if the spec is a valid sub-spec.
  
      If it is not, then raise a ValueError
      """
      err = (
          'Objects with "{0}" attribute cannot be used within {1}. '
          "Consider defining the {0} attribute in the {1} object instead."
      )
  
      if not isinstance(spec, (core.SchemaBase, dict)):
          raise ValueError("Only chart objects can be used in {0}.".format(classname))
      for attr in TOPLEVEL_ONLY_KEYS:
          if isinstance(spec, core.SchemaBase):
              val = getattr(spec, attr, Undefined)
          else:
              val = spec.get(attr, Undefined)
          if val is not Undefined:
              raise ValueError(err.format(attr, classname))
  
  
  def _check_if_can_be_layered(spec):
      """Check if the spec can be layered."""
  
      def _get(spec, attr):
          if isinstance(spec, core.SchemaBase):
              return spec._get(attr)
          else:
              return spec.get(attr, Undefined)
  
      encoding = _get(spec, "encoding")
      if encoding is not Undefined:
          for channel in ["row", "column", "facet"]:
              if _get(encoding, channel) is not Undefined:
                  raise ValueError("Faceted charts cannot be layered.")
      if isinstance(spec, (Chart, LayerChart)):
          return
  
      if not isinstance(spec, (core.SchemaBase, dict)):
          raise ValueError("Only chart objects can be layered.")
      if _get(spec, "facet") is not Undefined:
          raise ValueError("Faceted charts cannot be layered.")
      if isinstance(spec, FacetChart) or _get(spec, "facet") is not Undefined:
          raise ValueError("Faceted charts cannot be layered.")
      if isinstance(spec, RepeatChart) or _get(spec, "repeat") is not Undefined:
          raise ValueError("Repeat charts cannot be layered.")
      if isinstance(spec, ConcatChart) or _get(spec, "concat") is not Undefined:
          raise ValueError("Concatenated charts cannot be layered.")
      if isinstance(spec, HConcatChart) or _get(spec, "hconcat") is not Undefined:
          raise ValueError("Concatenated charts cannot be layered.")
      if isinstance(spec, VConcatChart) or _get(spec, "vconcat") is not Undefined:
          raise ValueError("Concatenated charts cannot be layered.")
  
  
  @utils.use_signature(core.TopLevelRepeatSpec)
  class RepeatChart(TopLevelMixin, core.TopLevelRepeatSpec):
      """A chart repeated across rows and columns with small changes"""
  
      # Because TopLevelRepeatSpec is defined as a union as of Vega-Lite schema 4.9,
      # we set the arguments explicitly here.
      # TODO: Should we instead use tools/schemapi/codegen._get_args?
      def __init__(
          self,
          repeat=Undefined,
          spec=Undefined,
          align=Undefined,
          autosize=Undefined,
          background=Undefined,
          bounds=Undefined,
          center=Undefined,
          columns=Undefined,
          config=Undefined,
          data=Undefined,
          datasets=Undefined,
          description=Undefined,
          name=Undefined,
          padding=Undefined,
          params=Undefined,
          resolve=Undefined,
          spacing=Undefined,
          title=Undefined,
          transform=Undefined,
          usermeta=Undefined,
          **kwds,
      ):
          _check_if_valid_subspec(spec, "RepeatChart")
          super(RepeatChart, self).__init__(
              repeat=repeat,
              spec=spec,
              align=align,
              autosize=autosize,
              background=background,
              bounds=bounds,
              center=center,
              columns=columns,
              config=config,
              data=data,
              datasets=datasets,
              description=description,
              name=name,
              padding=padding,
              params=params,
              resolve=resolve,
              spacing=spacing,
              title=title,
              transform=transform,
              usermeta=usermeta,
              **kwds,
          )
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          copy = self.copy(deep=False)
          copy.spec = copy.spec.interactive(name=name, bind_x=bind_x, bind_y=bind_y)
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to the chart."""
          if not selections or self.spec is Undefined:
              return self
          copy = self.copy()
          copy.spec = copy.spec.add_selection(*selections)
          return copy
  
  
  def repeat(repeater="repeat"):
      """Tie a channel to the row or column within a repeated chart
  
      The output of this should be passed to the ``field`` attribute of
      a channel.
  
      Parameters
      ----------
      repeater : {'row'|'column'|'repeat'|'layer'}
          The repeater to tie the field to. Default is 'repeat'.
  
      Returns
      -------
      repeat : RepeatRef object
      """
      if repeater not in ["row", "column", "repeat", "layer"]:
          raise ValueError("repeater must be one of ['row', 'column', 'repeat', 'layer']")
      return core.RepeatRef(repeat=repeater)
  
  
  @utils.use_signature(core.TopLevelConcatSpec)
  class ConcatChart(TopLevelMixin, core.TopLevelConcatSpec):
      """A chart with horizontally-concatenated facets"""
  
      def __init__(self, data=Undefined, concat=(), columns=Undefined, **kwargs):
          # TODO: move common data to top level?
          for spec in concat:
              _check_if_valid_subspec(spec, "ConcatChart")
          super(ConcatChart, self).__init__(
              data=data, concat=list(concat), columns=columns, **kwargs
          )
          self.data, self.concat = _combine_subchart_data(self.data, self.concat)
  
      def __ior__(self, other):
          _check_if_valid_subspec(other, "ConcatChart")
          self.concat.append(other)
          self.data, self.concat = _combine_subchart_data(self.data, self.concat)
          return self
  
      def __or__(self, other):
          copy = self.copy(deep=["concat"])
          copy |= other
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.concat:
              return self
          copy = self.copy()
          copy.concat = [chart.add_selection(*selections) for chart in copy.concat]
          return copy
  
  
  def concat(*charts, **kwargs):
      """Concatenate charts horizontally"""
      return ConcatChart(concat=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelHConcatSpec)
  class HConcatChart(TopLevelMixin, core.TopLevelHConcatSpec):
      """A chart with horizontally-concatenated facets"""
  
      def __init__(self, data=Undefined, hconcat=(), **kwargs):
          # TODO: move common data to top level?
          for spec in hconcat:
              _check_if_valid_subspec(spec, "HConcatChart")
          super(HConcatChart, self).__init__(data=data, hconcat=list(hconcat), **kwargs)
          self.data, self.hconcat = _combine_subchart_data(self.data, self.hconcat)
  
      def __ior__(self, other):
          _check_if_valid_subspec(other, "HConcatChart")
          self.hconcat.append(other)
          self.data, self.hconcat = _combine_subchart_data(self.data, self.hconcat)
          return self
  
      def __or__(self, other):
          copy = self.copy(deep=["hconcat"])
          copy |= other
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.hconcat:
              return self
          copy = self.copy()
          copy.hconcat = [chart.add_selection(*selections) for chart in copy.hconcat]
          return copy
  
  
  def hconcat(*charts, **kwargs):
      """Concatenate charts horizontally"""
      return HConcatChart(hconcat=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelVConcatSpec)
  class VConcatChart(TopLevelMixin, core.TopLevelVConcatSpec):
      """A chart with vertically-concatenated facets"""
  
      def __init__(self, data=Undefined, vconcat=(), **kwargs):
          # TODO: move common data to top level?
          for spec in vconcat:
              _check_if_valid_subspec(spec, "VConcatChart")
          super(VConcatChart, self).__init__(data=data, vconcat=list(vconcat), **kwargs)
          self.data, self.vconcat = _combine_subchart_data(self.data, self.vconcat)
  
      def __iand__(self, other):
          _check_if_valid_subspec(other, "VConcatChart")
          self.vconcat.append(other)
          self.data, self.vconcat = _combine_subchart_data(self.data, self.vconcat)
          return self
  
      def __and__(self, other):
          copy = self.copy(deep=["vconcat"])
          copy &= other
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.vconcat:
              return self
          copy = self.copy()
          copy.vconcat = [chart.add_selection(*selections) for chart in copy.vconcat]
          return copy
  
  
  def vconcat(*charts, **kwargs):
      """Concatenate charts vertically"""
      return VConcatChart(vconcat=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelLayerSpec)
  class LayerChart(TopLevelMixin, _EncodingMixin, core.TopLevelLayerSpec):
      """A Chart with layers within a single panel"""
  
      def __init__(self, data=Undefined, layer=(), **kwargs):
          # TODO: move common data to top level?
          # TODO: check for conflicting interaction
          for spec in layer:
              _check_if_valid_subspec(spec, "LayerChart")
              _check_if_can_be_layered(spec)
          super(LayerChart, self).__init__(data=data, layer=list(layer), **kwargs)
          self.data, self.layer = _combine_subchart_data(self.data, self.layer)
  
          # Some properties are not allowed within layer; we'll move to parent.
          layer_props = ("height", "width", "view")
          combined_dict, self.layer = _remove_layer_props(self, self.layer, layer_props)
  
          for prop in combined_dict:
              self[prop] = combined_dict[prop]
  
      def __iadd__(self, other):
          _check_if_valid_subspec(other, "LayerChart")
          _check_if_can_be_layered(other)
          self.layer.append(other)
          self.data, self.layer = _combine_subchart_data(self.data, self.layer)
          return self
  
      def __add__(self, other):
          copy = self.copy(deep=["layer"])
          copy += other
          return copy
  
      def add_layers(self, *layers):
          copy = self.copy(deep=["layer"])
          for layer in layers:
              copy += layer
          return copy
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          if not self.layer:
              raise ValueError(
                  "LayerChart: cannot call interactive() until a " "layer is defined"
              )
          copy = self.copy(deep=["layer"])
          copy.layer[0] = copy.layer[0].interactive(
              name=name, bind_x=bind_x, bind_y=bind_y
          )
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to all subcharts."""
          if not selections or not self.layer:
              return self
          copy = self.copy()
          copy.layer[0] = copy.layer[0].add_selection(*selections)
          return copy
  
  
  def layer(*charts, **kwargs):
      """layer multiple charts"""
      return LayerChart(layer=charts, **kwargs)
  
  
  @utils.use_signature(core.TopLevelFacetSpec)
  class FacetChart(TopLevelMixin, core.TopLevelFacetSpec):
      """A Chart with layers within a single panel"""
  
      def __init__(self, data=Undefined, spec=Undefined, facet=Undefined, **kwargs):
          _check_if_valid_subspec(spec, "FacetChart")
          super(FacetChart, self).__init__(data=data, spec=spec, facet=facet, **kwargs)
  
      def interactive(self, name=None, bind_x=True, bind_y=True):
          """Make chart axes scales interactive
  
          Parameters
          ----------
          name : string
              The parameter name to use for the axes scales. This name should be
              unique among all parameters within the chart.
          bind_x : boolean, default True
              If true, then bind the interactive scales to the x-axis
          bind_y : boolean, default True
              If true, then bind the interactive scales to the y-axis
  
          Returns
          -------
          chart :
              copy of self, with interactive axes added
  
          """
          copy = self.copy(deep=False)
          copy.spec = copy.spec.interactive(name=name, bind_x=bind_x, bind_y=bind_y)
          return copy
  
      def add_parameter(self, *params):
          """Add one or more parameters to the chart."""
          if not params:
              return self
          copy = self.copy(deep=["params"])
          if copy.params is Undefined:
              copy.params = []
  
          for s in params:
              copy.params.append(s.param)
          return copy
  
      def add_selection(self, *selections):
          """Add one or more selections to the chart."""
          if not selections or self.spec is Undefined:
              return self
          copy = self.copy()
          copy.spec = copy.spec.add_selection(*selections)
          return copy
  
  
  def topo_feature(url, feature, **kwargs):
      """A convenience function for extracting features from a topojson url
  
      Parameters
      ----------
      url : string
          An URL from which to load the data set.
  
      feature : string
          The name of the TopoJSON object set to convert to a GeoJSON feature collection. For
          example, in a map of the world, there may be an object set named `"countries"`.
          Using the feature property, we can extract this set and generate a GeoJSON feature
          object for each country.
  
      **kwargs :
          additional keywords passed to TopoDataFormat
      """
      return core.UrlData(
          url=url, format=core.TopoDataFormat(type="topojson", feature=feature, **kwargs)
      )
  
  
  def _combine_subchart_data(data, subcharts):
      def remove_data(subchart):
          if subchart.data is not Undefined:
              subchart = subchart.copy()
              subchart.data = Undefined
          return subchart
  
      if not subcharts:
          # No subcharts = nothing to do.
          pass
      elif data is Undefined:
          # Top level has no data; all subchart data must
          # be identical to proceed.
          subdata = subcharts[0].data
          if subdata is not Undefined and all(c.data is subdata for c in subcharts):
              data = subdata
              subcharts = [remove_data(c) for c in subcharts]
      else:
          # Top level has data; subchart data must be either
          # undefined or identical to proceed.
          if all(c.data is Undefined or c.data is data for c in subcharts):
              subcharts = [remove_data(c) for c in subcharts]
  
      return data, subcharts
  
  
  def _remove_layer_props(chart, subcharts, layer_props):
      def remove_prop(subchart, prop):
          # If subchart is a UnitSpec, then subchart["height"] raises a KeyError
          try:
              if subchart[prop] is not Undefined:
                  subchart = subchart.copy()
                  subchart[prop] = Undefined
          except KeyError:
              pass
          return subchart
  
      output_dict = {}
  
      if not subcharts:
          # No subcharts = nothing to do.
          return output_dict, subcharts
  
      for prop in layer_props:
          if chart[prop] is Undefined:
              # Top level does not have this prop.
              # Check for consistent props within the subcharts.
              values = []
              for c in subcharts:
                  # If c is a UnitSpec, then c["height"] raises a KeyError.
                  try:
                      val = c[prop]
                      if val is not Undefined:
                          values.append(val)
                  except KeyError:
                      pass
              if len(values) == 0:
                  pass
              elif all(v is values[0] for v in values[1:]):
                  output_dict[prop] = values[0]
              else:
                  raise ValueError(f"There are inconsistent values {values} for {prop}")
          else:
              # Top level has this prop; subchart props must be either
              # Undefined or identical to proceed.
              if all(c[prop] is Undefined or c[prop] is chart[prop] for c in subcharts):
                  output_dict[prop] = chart[prop]
              else:
                  raise ValueError(f"There are inconsistent values {values} for {prop}")
          subcharts = [remove_prop(c, prop) for c in subcharts]
  
      return output_dict, subcharts
  
  
  @utils.use_signature(core.SequenceParams)
  def sequence(start, stop=None, step=Undefined, as_=Undefined, **kwds):
      """Sequence generator."""
      if stop is None:
          start, stop = 0, start
      params = core.SequenceParams(start=start, stop=stop, step=step, **{"as": as_})
      return core.SequenceGenerator(sequence=params, **kwds)
  
  
  @utils.use_signature(core.GraticuleParams)
  def graticule(**kwds):
      """Graticule generator."""
      if not kwds:
          # graticule: True indicates default parameters
          graticule = True
      else:
          graticule = core.GraticuleParams(**kwds)
      return core.GraticuleGenerator(graticule=graticule)
  
  
  def sphere():
      """Sphere generator."""
      return core.SphereGenerator(sphere=True)
  
files: altair/vegalite/v5/api.py
files: """Unit tests for altair API"""
  
  import io
  import json
  import operator
  import os
  import pathlib
  import tempfile
  
  import jsonschema
  import pytest
  import pandas as pd
  
  import altair.vegalite.v5 as alt
  
  try:
      import altair_saver  # noqa: F401
  except ImportError:
      altair_saver = None
  
  
  def getargs(*args, **kwargs):
      return args, kwargs
  
  
  OP_DICT = {
      "layer": operator.add,
      "hconcat": operator.or_,
      "vconcat": operator.and_,
  }
  
  
  def _make_chart_type(chart_type):
      data = pd.DataFrame(
          {
              "x": [28, 55, 43, 91, 81, 53, 19, 87],
              "y": [43, 91, 81, 53, 19, 87, 52, 28],
              "color": list("AAAABBBB"),
          }
      )
      base = (
          alt.Chart(data)
          .mark_point()
          .encode(
              x="x",
              y="y",
              color="color",
          )
      )
  
      if chart_type in ["layer", "hconcat", "vconcat", "concat"]:
          func = getattr(alt, chart_type)
          return func(base.mark_square(), base.mark_circle())
      elif chart_type == "facet":
          return base.facet("color")
      elif chart_type == "facet_encoding":
          return base.encode(facet="color")
      elif chart_type == "repeat":
          return base.encode(alt.X(alt.repeat(), type="quantitative")).repeat(["x", "y"])
      elif chart_type == "chart":
          return base
      else:
          raise ValueError("chart_type='{}' is not recognized".format(chart_type))
  
  
  @pytest.fixture
  def basic_chart():
      data = pd.DataFrame(
          {
              "a": ["A", "B", "C", "D", "E", "F", "G", "H", "I"],
              "b": [28, 55, 43, 91, 81, 53, 19, 87, 52],
          }
      )
  
      return alt.Chart(data).mark_bar().encode(x="a", y="b")
  
  
  def test_chart_data_types():
      def Chart(data):
          return alt.Chart(data).mark_point().encode(x="x:Q", y="y:Q")
  
      # Url Data
      data = "/path/to/my/data.csv"
      dct = Chart(data).to_dict()
      assert dct["data"] == {"url": data}
  
      # Dict Data
      data = {"values": [{"x": 1, "y": 2}, {"x": 2, "y": 3}]}
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = Chart(data).to_dict()
      assert dct["data"] == data
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = Chart(data).to_dict()
      name = dct["data"]["name"]
      assert dct["datasets"][name] == data["values"]
  
      # DataFrame data
      data = pd.DataFrame({"x": range(5), "y": range(5)})
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = Chart(data).to_dict()
      assert dct["data"]["values"] == data.to_dict(orient="records")
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = Chart(data).to_dict()
      name = dct["data"]["name"]
      assert dct["datasets"][name] == data.to_dict(orient="records")
  
      # Named data object
      data = alt.NamedData(name="Foo")
      dct = Chart(data).to_dict()
      assert dct["data"] == {"name": "Foo"}
  
  
  def test_chart_infer_types():
      data = pd.DataFrame(
          {
              "x": pd.date_range("2012", periods=10, freq="Y"),
              "y": range(10),
              "c": list("abcabcabca"),
          }
      )
  
      def _check_encodings(chart):
          dct = chart.to_dict()
          assert dct["encoding"]["x"]["type"] == "temporal"
          assert dct["encoding"]["x"]["field"] == "x"
          assert dct["encoding"]["y"]["type"] == "quantitative"
          assert dct["encoding"]["y"]["field"] == "y"
          assert dct["encoding"]["color"]["type"] == "nominal"
          assert dct["encoding"]["color"]["field"] == "c"
  
      # Pass field names by keyword
      chart = alt.Chart(data).mark_point().encode(x="x", y="y", color="c")
      _check_encodings(chart)
  
      # pass Channel objects by keyword
      chart = (
          alt.Chart(data)
          .mark_point()
          .encode(x=alt.X("x"), y=alt.Y("y"), color=alt.Color("c"))
      )
      _check_encodings(chart)
  
      # pass Channel objects by value
      chart = alt.Chart(data).mark_point().encode(alt.X("x"), alt.Y("y"), alt.Color("c"))
      _check_encodings(chart)
  
      # override default types
      chart = (
          alt.Chart(data)
          .mark_point()
          .encode(alt.X("x", type="nominal"), alt.Y("y", type="ordinal"))
      )
      dct = chart.to_dict()
      assert dct["encoding"]["x"]["type"] == "nominal"
      assert dct["encoding"]["y"]["type"] == "ordinal"
  
  
  @pytest.mark.parametrize(
      "args, kwargs",
      [
          getargs(detail=["value:Q", "name:N"], tooltip=["value:Q", "name:N"]),
          getargs(detail=["value", "name"], tooltip=["value", "name"]),
          getargs(alt.Detail(["value:Q", "name:N"]), alt.Tooltip(["value:Q", "name:N"])),
          getargs(alt.Detail(["value", "name"]), alt.Tooltip(["value", "name"])),
          getargs(
              [alt.Detail("value:Q"), alt.Detail("name:N")],
              [alt.Tooltip("value:Q"), alt.Tooltip("name:N")],
          ),
          getargs(
              [alt.Detail("value"), alt.Detail("name")],
              [alt.Tooltip("value"), alt.Tooltip("name")],
          ),
      ],
  )
  def test_multiple_encodings(args, kwargs):
      df = pd.DataFrame({"value": [1, 2, 3], "name": ["A", "B", "C"]})
      encoding_dct = [
          {"field": "value", "type": "quantitative"},
          {"field": "name", "type": "nominal"},
      ]
      chart = alt.Chart(df).mark_point().encode(*args, **kwargs)
      dct = chart.to_dict()
      assert dct["encoding"]["detail"] == encoding_dct
      assert dct["encoding"]["tooltip"] == encoding_dct
  
  
  def test_chart_operations():
      data = pd.DataFrame(
          {
              "x": pd.date_range("2012", periods=10, freq="Y"),
              "y": range(10),
              "c": list("abcabcabca"),
          }
      )
      chart1 = alt.Chart(data).mark_line().encode(x="x", y="y", color="c")
      chart2 = chart1.mark_point()
      chart3 = chart1.mark_circle()
      chart4 = chart1.mark_square()
  
      chart = chart1 + chart2 + chart3
      assert isinstance(chart, alt.LayerChart)
      assert len(chart.layer) == 3
      chart += chart4
      assert len(chart.layer) == 4
  
      chart = chart1 | chart2 | chart3
      assert isinstance(chart, alt.HConcatChart)
      assert len(chart.hconcat) == 3
      chart |= chart4
      assert len(chart.hconcat) == 4
  
      chart = chart1 & chart2 & chart3
      assert isinstance(chart, alt.VConcatChart)
      assert len(chart.vconcat) == 3
      chart &= chart4
      assert len(chart.vconcat) == 4
  
  
  def test_selection_to_dict():
      brush = alt.selection(type="interval")
  
      # test some value selections
      # Note: X and Y cannot have conditions
      alt.Chart("path/to/data.json").mark_point().encode(
          color=alt.condition(brush, alt.ColorValue("red"), alt.ColorValue("blue")),
          opacity=alt.condition(brush, alt.value(0.5), alt.value(1.0)),
          text=alt.condition(brush, alt.TextValue("foo"), alt.value("bar")),
      ).to_dict()
  
      # test some field selections
      # Note: X and Y cannot have conditions
      # Conditions cannot both be fields
      alt.Chart("path/to/data.json").mark_point().encode(
          color=alt.condition(brush, alt.Color("col1:N"), alt.value("blue")),
          opacity=alt.condition(brush, "col1:N", alt.value(0.5)),
          text=alt.condition(brush, alt.value("abc"), alt.Text("col2:N")),
          size=alt.condition(brush, alt.value(20), "col2:N"),
      ).to_dict()
  
  
  def test_selection_expression():
      selection = alt.selection_single(fields=["value"])
  
      assert isinstance(selection.value, alt.expr.Expression)
      assert selection.value.to_dict() == "{0}.value".format(selection.name)
  
      assert isinstance(selection["value"], alt.expr.Expression)
      assert selection["value"].to_dict() == "{0}['value']".format(selection.name)
  
      with pytest.raises(AttributeError):
          selection.__magic__
  
  
  @pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf"])
  def test_save(format, basic_chart):
      if format in ["pdf", "png"]:
          out = io.BytesIO()
          mode = "rb"
      else:
          out = io.StringIO()
          mode = "r"
  
      if format in ["svg", "png", "pdf"]:
          if not altair_saver:
              with pytest.raises(ValueError) as err:
                  basic_chart.save(out, format=format)
              assert "github.com/altair-viz/altair_saver" in str(err.value)
              return
          elif format not in altair_saver.available_formats():
              with pytest.raises(ValueError) as err:
                  basic_chart.save(out, format=format)
              assert f"No enabled saver found that supports format='{format}'" in str(
                  err.value
              )
              return
  
      basic_chart.save(out, format=format)
      out.seek(0)
      content = out.read()
  
      if format == "json":
          assert "$schema" in json.loads(content)
      if format == "html":
          assert content.startswith("<!DOCTYPE html>")
  
      fid, filename = tempfile.mkstemp(suffix="." + format)
      os.close(fid)
  
      # test both string filenames and pathlib.Paths
      for fp in [filename, pathlib.Path(filename)]:
          try:
              basic_chart.save(fp)
              with open(fp, mode) as f:
                  assert f.read()[:1000] == content[:1000]
          finally:
              os.remove(fp)
  
  
  def test_facet_basic():
      # wrapped facet
      chart1 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x="x:Q",
              y="y:Q",
          )
          .facet("category:N", columns=2)
      )
  
      dct1 = chart1.to_dict()
  
      assert dct1["facet"] == alt.Facet("category:N").to_dict()
      assert dct1["columns"] == 2
      assert dct1["data"] == alt.UrlData("data.csv").to_dict()
  
      # explicit row/col facet
      chart2 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x="x:Q",
              y="y:Q",
          )
          .facet(row="category1:Q", column="category2:Q")
      )
  
      dct2 = chart2.to_dict()
  
      assert dct2["facet"]["row"] == alt.Facet("category1:Q").to_dict()
      assert dct2["facet"]["column"] == alt.Facet("category2:Q").to_dict()
      assert "columns" not in dct2
      assert dct2["data"] == alt.UrlData("data.csv").to_dict()
  
  
  def test_facet_parse():
      chart = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(x="x:Q", y="y:Q")
          .facet(row="row:N", column="column:O")
      )
      dct = chart.to_dict()
      assert dct["data"] == {"url": "data.csv"}
      assert "data" not in dct["spec"]
      assert dct["facet"] == {
          "column": {"field": "column", "type": "ordinal"},
          "row": {"field": "row", "type": "nominal"},
      }
  
  
  def test_facet_parse_data():
      data = pd.DataFrame({"x": range(5), "y": range(5), "row": list("abcab")})
      chart = (
          alt.Chart(data)
          .mark_point()
          .encode(x="x", y="y:O")
          .facet(row="row", column="column:O")
      )
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = chart.to_dict()
      assert "values" in dct["data"]
      assert "data" not in dct["spec"]
      assert dct["facet"] == {
          "column": {"field": "column", "type": "ordinal"},
          "row": {"field": "row", "type": "nominal"},
      }
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = chart.to_dict()
      assert "datasets" in dct
      assert "name" in dct["data"]
      assert "data" not in dct["spec"]
      assert dct["facet"] == {
          "column": {"field": "column", "type": "ordinal"},
          "row": {"field": "row", "type": "nominal"},
      }
  
  
  def test_selection():
      # test instantiation of selections
      interval = alt.selection_interval(name="selec_1")
      assert interval.selection.type == "interval"
      assert interval.name == "selec_1"
  
      single = alt.selection_single(name="selec_2")
      assert single.selection.type == "single"
      assert single.name == "selec_2"
  
      multi = alt.selection_multi(name="selec_3")
      assert multi.selection.type == "multi"
      assert multi.name == "selec_3"
  
      # test adding to chart
      chart = alt.Chart().add_selection(single)
      chart = chart.add_selection(multi, interval)
      assert set(chart.selection.keys()) == {"selec_1", "selec_2", "selec_3"}
  
      # test logical operations
      assert isinstance(single & multi, alt.Selection)
      assert isinstance(single | multi, alt.Selection)
      assert isinstance(~single, alt.Selection)
      assert isinstance((single & multi)[0].group, alt.SelectionAnd)
      assert isinstance((single | multi)[0].group, alt.SelectionOr)
      assert isinstance((~single)[0].group, alt.SelectionNot)
  
      # test that default names increment (regression for #1454)
      sel1 = alt.selection_single()
      sel2 = alt.selection_multi()
      sel3 = alt.selection_interval()
      names = {s.name for s in (sel1, sel2, sel3)}
      assert len(names) == 3
  
  
  def test_transforms():
      # aggregate transform
      agg1 = alt.AggregatedFieldDef(**{"as": "x1", "op": "mean", "field": "y"})
      agg2 = alt.AggregatedFieldDef(**{"as": "x2", "op": "median", "field": "z"})
      chart = alt.Chart().transform_aggregate([agg1], ["foo"], x2="median(z)")
      kwds = dict(aggregate=[agg1, agg2], groupby=["foo"])
      assert chart.transform == [alt.AggregateTransform(**kwds)]
  
      # bin transform
      chart = alt.Chart().transform_bin("binned", field="field", bin=True)
      kwds = {"as": "binned", "field": "field", "bin": True}
      assert chart.transform == [alt.BinTransform(**kwds)]
  
      # calcualte transform
      chart = alt.Chart().transform_calculate("calc", "datum.a * 4")
      kwds = {"as": "calc", "calculate": "datum.a * 4"}
      assert chart.transform == [alt.CalculateTransform(**kwds)]
  
      # density transform
      chart = alt.Chart().transform_density("x", as_=["value", "density"])
      kwds = {"as": ["value", "density"], "density": "x"}
      assert chart.transform == [alt.DensityTransform(**kwds)]
  
      # filter transform
      chart = alt.Chart().transform_filter("datum.a < 4")
      assert chart.transform == [alt.FilterTransform(filter="datum.a < 4")]
  
      # flatten transform
      chart = alt.Chart().transform_flatten(["A", "B"], ["X", "Y"])
      kwds = {"as": ["X", "Y"], "flatten": ["A", "B"]}
      assert chart.transform == [alt.FlattenTransform(**kwds)]
  
      # fold transform
      chart = alt.Chart().transform_fold(["A", "B", "C"], as_=["key", "val"])
      kwds = {"as": ["key", "val"], "fold": ["A", "B", "C"]}
      assert chart.transform == [alt.FoldTransform(**kwds)]
  
      # impute transform
      chart = alt.Chart().transform_impute("field", "key", groupby=["x"])
      kwds = {"impute": "field", "key": "key", "groupby": ["x"]}
      assert chart.transform == [alt.ImputeTransform(**kwds)]
  
      # joinaggregate transform
      chart = alt.Chart().transform_joinaggregate(min="min(x)", groupby=["key"])
      kwds = {
          "joinaggregate": [
              alt.JoinAggregateFieldDef(field="x", op="min", **{"as": "min"})
          ],
          "groupby": ["key"],
      }
      assert chart.transform == [alt.JoinAggregateTransform(**kwds)]
  
      # loess transform
      chart = alt.Chart().transform_loess("x", "y", as_=["xx", "yy"])
      kwds = {"on": "x", "loess": "y", "as": ["xx", "yy"]}
      assert chart.transform == [alt.LoessTransform(**kwds)]
  
      # lookup transform (data)
      lookup_data = alt.LookupData(alt.UrlData("foo.csv"), "id", ["rate"])
      chart = alt.Chart().transform_lookup("a", from_=lookup_data, as_="a", default="b")
      kwds = {"from": lookup_data, "as": "a", "lookup": "a", "default": "b"}
      assert chart.transform == [alt.LookupTransform(**kwds)]
  
      # lookup transform (selection)
      lookup_selection = alt.LookupSelection(key="key", selection="sel")
      chart = alt.Chart().transform_lookup(
          "a", from_=lookup_selection, as_="a", default="b"
      )
      kwds = {"from": lookup_selection, "as": "a", "lookup": "a", "default": "b"}
      assert chart.transform == [alt.LookupTransform(**kwds)]
  
      # pivot transform
      chart = alt.Chart().transform_pivot("x", "y")
      assert chart.transform == [alt.PivotTransform(pivot="x", value="y")]
  
      # quantile transform
      chart = alt.Chart().transform_quantile("x", as_=["prob", "value"])
      kwds = {"quantile": "x", "as": ["prob", "value"]}
      assert chart.transform == [alt.QuantileTransform(**kwds)]
  
      # regression transform
      chart = alt.Chart().transform_regression("x", "y", as_=["xx", "yy"])
      kwds = {"on": "x", "regression": "y", "as": ["xx", "yy"]}
      assert chart.transform == [alt.RegressionTransform(**kwds)]
  
      # sample transform
      chart = alt.Chart().transform_sample()
      assert chart.transform == [alt.SampleTransform(1000)]
  
      # stack transform
      chart = alt.Chart().transform_stack("stacked", "x", groupby=["y"])
      assert chart.transform == [
          alt.StackTransform(stack="x", groupby=["y"], **{"as": "stacked"})
      ]
  
      # timeUnit transform
      chart = alt.Chart().transform_timeunit("foo", field="x", timeUnit="date")
      kwds = {"as": "foo", "field": "x", "timeUnit": "date"}
      assert chart.transform == [alt.TimeUnitTransform(**kwds)]
  
      # window transform
      chart = alt.Chart().transform_window(xsum="sum(x)", ymin="min(y)", frame=[None, 0])
      window = [
          alt.WindowFieldDef(**{"as": "xsum", "field": "x", "op": "sum"}),
          alt.WindowFieldDef(**{"as": "ymin", "field": "y", "op": "min"}),
      ]
  
      # kwargs don't maintain order in Python < 3.6, so window list can
      # be reversed
      assert chart.transform == [
          alt.WindowTransform(frame=[None, 0], window=window)
      ] or chart.transform == [alt.WindowTransform(frame=[None, 0], window=window[::-1])]
  
  
  def test_filter_transform_selection_predicates():
      selector1 = alt.selection_interval(name="s1")
      selector2 = alt.selection_interval(name="s2")
      base = alt.Chart("data.txt").mark_point()
  
      chart = base.transform_filter(selector1)
      assert chart.to_dict()["transform"] == [{'filter': {'param': 's1'}}]
  
      chart = base.transform_filter(~selector1)
      assert chart.to_dict()["transform"] == [{'filter': {'not': {'param': 's1'}}}]
  
      chart = base.transform_filter(selector1 & selector2)
      assert chart.to_dict()["transform"] == [
          {'filter': {'and': [{'param': 's1'}, {'param': 's2'}]}}
      ]
  
      chart = base.transform_filter(selector1 | selector2)
      assert chart.to_dict()["transform"] == [
          {'filter': {'or': [{'param': 's1'}, {'param': 's2'}]}}
      ]
  
      chart = base.transform_filter(selector1 | ~selector2)
      assert chart.to_dict()["transform"] == [
          {'filter': {'or': [{'param': 's1'}, {'not': {'param': 's2'}}]}}
      ]
  
      chart = base.transform_filter(~selector1 | ~selector2)
      assert chart.to_dict()["transform"] == [
          {'filter': {'or': [{'not': {'param': 's1'}}, {'not': {'param': 's2'}}]}}
      ]
  
      chart = base.transform_filter(~(selector1 & selector2))
      assert chart.to_dict()["transform"] == [
          {'filter': {'not': {'and': [{'param': 's1'}, {'param': 's2'}]}}}
      ]
  
  
  def test_resolve_methods():
      chart = alt.LayerChart().resolve_axis(x="shared", y="independent")
      assert chart.resolve == alt.Resolve(
          axis=alt.AxisResolveMap(x="shared", y="independent")
      )
  
      chart = alt.LayerChart().resolve_legend(color="shared", fill="independent")
      assert chart.resolve == alt.Resolve(
          legend=alt.LegendResolveMap(color="shared", fill="independent")
      )
  
      chart = alt.LayerChart().resolve_scale(x="shared", y="independent")
      assert chart.resolve == alt.Resolve(
          scale=alt.ScaleResolveMap(x="shared", y="independent")
      )
  
  
  def test_layer_encodings():
      chart = alt.LayerChart().encode(x="column:Q")
      assert chart.encoding.x == alt.X(shorthand="column:Q")
  
  
  def test_add_selection():
      selections = [
          alt.selection_interval(),
          alt.selection_single(),
          alt.selection_multi(),
      ]
      chart = (
          alt.Chart()
          .mark_point()
          .add_selection(selections[0])
          .add_selection(selections[1], selections[2])
      )
      expected = {s.name: s.selection for s in selections}
      assert chart.selection == expected
  
  
  def test_repeat_add_selections():
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = base.add_selection(selection).repeat(list("ABC"))
      chart2 = base.repeat(list("ABC")).add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  def test_facet_add_selections():
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = base.add_selection(selection).facet("val:Q")
      chart2 = base.facet("val:Q").add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  def test_layer_add_selection():
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = alt.layer(base.add_selection(selection), base)
      chart2 = alt.layer(base, base).add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  @pytest.mark.parametrize("charttype", [alt.concat, alt.hconcat, alt.vconcat])
  def test_compound_add_selections(charttype):
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = charttype(base.add_selection(selection), base.add_selection(selection))
      chart2 = charttype(base, base).add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  def test_selection_property():
      sel = alt.selection_interval()
      chart = alt.Chart("data.csv").mark_point().properties(selection=sel)
  
      assert list(chart["selection"].keys()) == [sel.name]
  
  
  def test_LookupData():
      df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
      lookup = alt.LookupData(data=df, key="x")
  
      dct = lookup.to_dict()
      assert dct["key"] == "x"
      assert dct["data"] == {
          "values": [{"x": 1, "y": 4}, {"x": 2, "y": 5}, {"x": 3, "y": 6}]
      }
  
  
  def test_themes():
      chart = alt.Chart("foo.txt").mark_point()
  
      with alt.themes.enable("default"):
          assert chart.to_dict()["config"] == {
              "view": {"continuousWidth": 400, "continuousHeight": 300}
          }
  
      with alt.themes.enable("opaque"):
          assert chart.to_dict()["config"] == {
              "background": "white",
              "view": {"continuousWidth": 400, "continuousHeight": 300},
          }
  
      with alt.themes.enable("none"):
          assert "config" not in chart.to_dict()
  
  
  def test_chart_from_dict():
      base = alt.Chart("data.csv").mark_point().encode(x="x:Q", y="y:Q")
  
      charts = [
          base,
          base + base,
          base | base,
          base & base,
          base.facet("c:N"),
          (base + base).facet(row="c:N", data="data.csv"),
          base.repeat(["c", "d"]),
          (base + base).repeat(row=["c", "d"]),
      ]
  
      for chart in charts:
          chart_out = alt.Chart.from_dict(chart.to_dict())
          assert type(chart_out) is type(chart)
  
      # test that an invalid spec leads to a schema validation error
      with pytest.raises(jsonschema.ValidationError):
          alt.Chart.from_dict({"invalid": "spec"})
  
  
  def test_consolidate_datasets(basic_chart):
      subchart1 = basic_chart
      subchart2 = basic_chart.copy()
      subchart2.data = basic_chart.data.copy()
      chart = subchart1 | subchart2
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct_consolidated = chart.to_dict()
  
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct_standard = chart.to_dict()
  
      assert "datasets" in dct_consolidated
      assert "datasets" not in dct_standard
  
      datasets = dct_consolidated["datasets"]
  
      # two dataset copies should be recognized as duplicates
      assert len(datasets) == 1
  
      # make sure data matches original & names are correct
      name, data = datasets.popitem()
  
      for spec in dct_standard["hconcat"]:
          assert spec["data"]["values"] == data
  
      for spec in dct_consolidated["hconcat"]:
          assert spec["data"] == {"name": name}
  
  
  def test_consolidate_InlineData():
      data = alt.InlineData(
          values=[{"a": 1, "b": 1}, {"a": 2, "b": 2}], format={"type": "csv"}
      )
      chart = alt.Chart(data).mark_point()
  
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = chart.to_dict()
      assert dct["data"]["format"] == data.format
      assert dct["data"]["values"] == data.values
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = chart.to_dict()
      assert dct["data"]["format"] == data.format
      assert list(dct["datasets"].values())[0] == data.values
  
      data = alt.InlineData(values=[], name="runtime_data")
      chart = alt.Chart(data).mark_point()
  
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = chart.to_dict()
      assert dct["data"] == data.to_dict()
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = chart.to_dict()
      assert dct["data"] == data.to_dict()
  
  
  def test_repeat():
      # wrapped repeat
      chart1 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x=alt.X(alt.repeat(), type="quantitative"),
              y="y:Q",
          )
          .repeat(["A", "B", "C", "D"], columns=2)
      )
  
      dct1 = chart1.to_dict()
  
      assert dct1["repeat"] == ["A", "B", "C", "D"]
      assert dct1["columns"] == 2
      assert dct1["spec"]["encoding"]["x"]["field"] == {"repeat": "repeat"}
  
      # explicit row/col repeat
      chart2 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x=alt.X(alt.repeat("row"), type="quantitative"),
              y=alt.Y(alt.repeat("column"), type="quantitative"),
          )
          .repeat(row=["A", "B", "C"], column=["C", "B", "A"])
      )
  
      dct2 = chart2.to_dict()
  
      assert dct2["repeat"] == {"row": ["A", "B", "C"], "column": ["C", "B", "A"]}
      assert "columns" not in dct2
      assert dct2["spec"]["encoding"]["x"]["field"] == {"repeat": "row"}
      assert dct2["spec"]["encoding"]["y"]["field"] == {"repeat": "column"}
  
  
  def test_data_property():
      data = pd.DataFrame({"x": [1, 2, 3], "y": list("ABC")})
      chart1 = alt.Chart(data).mark_point()
      chart2 = alt.Chart().mark_point().properties(data=data)
  
      assert chart1.to_dict() == chart2.to_dict()
  
  
  @pytest.mark.parametrize("method", ["layer", "hconcat", "vconcat", "concat"])
  @pytest.mark.parametrize(
      "data", ["data.json", pd.DataFrame({"x": range(3), "y": list("abc")})]
  )
  def test_subcharts_with_same_data(method, data):
      func = getattr(alt, method)
  
      point = alt.Chart(data).mark_point().encode(x="x:Q", y="y:Q")
      line = point.mark_line()
      text = point.mark_text()
  
      chart1 = func(point, line, text)
      assert chart1.data is not alt.Undefined
      assert all(c.data is alt.Undefined for c in getattr(chart1, method))
  
      if method != "concat":
          op = OP_DICT[method]
          chart2 = op(op(point, line), text)
          assert chart2.data is not alt.Undefined
          assert all(c.data is alt.Undefined for c in getattr(chart2, method))
  
  
  @pytest.mark.parametrize("method", ["layer", "hconcat", "vconcat", "concat"])
  @pytest.mark.parametrize(
      "data", ["data.json", pd.DataFrame({"x": range(3), "y": list("abc")})]
  )
  def test_subcharts_different_data(method, data):
      func = getattr(alt, method)
  
      point = alt.Chart(data).mark_point().encode(x="x:Q", y="y:Q")
      otherdata = alt.Chart("data.csv").mark_point().encode(x="x:Q", y="y:Q")
      nodata = alt.Chart().mark_point().encode(x="x:Q", y="y:Q")
  
      chart1 = func(point, otherdata)
      assert chart1.data is alt.Undefined
      assert getattr(chart1, method)[0].data is data
  
      chart2 = func(point, nodata)
      assert chart2.data is alt.Undefined
      assert getattr(chart2, method)[0].data is data
  
  
  def test_layer_facet(basic_chart):
      chart = (basic_chart + basic_chart).facet(row="row:Q")
      assert chart.data is not alt.Undefined
      assert chart.spec.data is alt.Undefined
      for layer in chart.spec.layer:
          assert layer.data is alt.Undefined
  
      dct = chart.to_dict()
      assert "data" in dct
  
  
  def test_layer_errors():
      toplevel_chart = alt.Chart("data.txt").mark_point().configure_legend(columns=2)
  
      facet_chart1 = alt.Chart("data.txt").mark_point().encode(facet="row:Q")
  
      facet_chart2 = alt.Chart("data.txt").mark_point().facet("row:Q")
  
      repeat_chart = alt.Chart("data.txt").mark_point().repeat(["A", "B", "C"])
  
      simple_chart = alt.Chart("data.txt").mark_point()
  
      with pytest.raises(ValueError) as err:
          toplevel_chart + simple_chart
      assert str(err.value).startswith(
          'Objects with "config" attribute cannot be used within LayerChart.'
      )
  
      with pytest.raises(ValueError) as err:
          repeat_chart + simple_chart
      assert str(err.value) == "Repeat charts cannot be layered."
  
      with pytest.raises(ValueError) as err:
          facet_chart1 + simple_chart
      assert str(err.value) == "Faceted charts cannot be layered."
  
      with pytest.raises(ValueError) as err:
          alt.layer(simple_chart) + facet_chart2
      assert str(err.value) == "Faceted charts cannot be layered."
  
  
  @pytest.mark.parametrize(
      "chart_type",
      ["layer", "hconcat", "vconcat", "concat", "facet", "facet_encoding", "repeat"],
  )
  def test_resolve(chart_type):
      chart = _make_chart_type(chart_type)
      chart = (
          chart.resolve_scale(
              x="independent",
          )
          .resolve_legend(color="independent")
          .resolve_axis(y="independent")
      )
      dct = chart.to_dict()
      assert dct["resolve"] == {
          "scale": {"x": "independent"},
          "legend": {"color": "independent"},
          "axis": {"y": "independent"},
      }
  
  
  # TODO: test vconcat, hconcat, concat, facet_encoding when schema allows them.
  # This is blocked by https://github.com/vega/vega-lite/issues/5261
  @pytest.mark.parametrize("chart_type", ["chart", "layer"])
  @pytest.mark.parametrize("facet_arg", [None, "facet", "row", "column"])
  def test_facet(chart_type, facet_arg):
      chart = _make_chart_type(chart_type)
      if facet_arg is None:
          chart = chart.facet("color:N", columns=2)
      else:
          chart = chart.facet(**{facet_arg: "color:N", "columns": 2})
      dct = chart.to_dict()
  
      assert "spec" in dct
      assert dct["columns"] == 2
      expected = {"field": "color", "type": "nominal"}
      if facet_arg is None or facet_arg == "facet":
          assert dct["facet"] == expected
      else:
          assert dct["facet"][facet_arg] == expected
  
  
  def test_sequence():
      data = alt.sequence(100)
      assert data.to_dict() == {"sequence": {"start": 0, "stop": 100}}
  
      data = alt.sequence(5, 10)
      assert data.to_dict() == {"sequence": {"start": 5, "stop": 10}}
  
      data = alt.sequence(0, 1, 0.1, as_="x")
      assert data.to_dict() == {
          "sequence": {"start": 0, "stop": 1, "step": 0.1, "as": "x"}
      }
  
  
  def test_graticule():
      data = alt.graticule()
      assert data.to_dict() == {"graticule": True}
  
      data = alt.graticule(step=[15, 15])
      assert data.to_dict() == {"graticule": {"step": [15, 15]}}
  
  
  def test_sphere():
      data = alt.sphere()
      assert data.to_dict() == {"sphere": True}
  
  
  def test_validate_dataset():
      d = {"data": {"values": [{}]}, "mark": {"type": "point"}}
  
      chart = alt.Chart.from_dict(d)
      jsn = chart.to_json()
  
      assert jsn
  
files: altair/vegalite/v5/tests/test_api.py
files: """Unit tests for altair API"""
  
  import io
  import json
  import operator
  import os
  import pathlib
  import tempfile
  
  import jsonschema
  import pytest
  import pandas as pd
  
  import altair.vegalite.v5 as alt
  
  try:
      import altair_saver  # noqa: F401
  except ImportError:
      altair_saver = None
  
  
  def getargs(*args, **kwargs):
      return args, kwargs
  
  
  OP_DICT = {
      "layer": operator.add,
      "hconcat": operator.or_,
      "vconcat": operator.and_,
  }
  
  
  def _make_chart_type(chart_type):
      data = pd.DataFrame(
          {
              "x": [28, 55, 43, 91, 81, 53, 19, 87],
              "y": [43, 91, 81, 53, 19, 87, 52, 28],
              "color": list("AAAABBBB"),
          }
      )
      base = (
          alt.Chart(data)
          .mark_point()
          .encode(
              x="x",
              y="y",
              color="color",
          )
      )
  
      if chart_type in ["layer", "hconcat", "vconcat", "concat"]:
          func = getattr(alt, chart_type)
          return func(base.mark_square(), base.mark_circle())
      elif chart_type == "facet":
          return base.facet("color")
      elif chart_type == "facet_encoding":
          return base.encode(facet="color")
      elif chart_type == "repeat":
          return base.encode(alt.X(alt.repeat(), type="quantitative")).repeat(["x", "y"])
      elif chart_type == "chart":
          return base
      else:
          raise ValueError("chart_type='{}' is not recognized".format(chart_type))
  
  
  @pytest.fixture
  def basic_chart():
      data = pd.DataFrame(
          {
              "a": ["A", "B", "C", "D", "E", "F", "G", "H", "I"],
              "b": [28, 55, 43, 91, 81, 53, 19, 87, 52],
          }
      )
  
      return alt.Chart(data).mark_bar().encode(x="a", y="b")
  
  
  def test_chart_data_types():
      def Chart(data):
          return alt.Chart(data).mark_point().encode(x="x:Q", y="y:Q")
  
      # Url Data
      data = "/path/to/my/data.csv"
      dct = Chart(data).to_dict()
      assert dct["data"] == {"url": data}
  
      # Dict Data
      data = {"values": [{"x": 1, "y": 2}, {"x": 2, "y": 3}]}
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = Chart(data).to_dict()
      assert dct["data"] == data
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = Chart(data).to_dict()
      name = dct["data"]["name"]
      assert dct["datasets"][name] == data["values"]
  
      # DataFrame data
      data = pd.DataFrame({"x": range(5), "y": range(5)})
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = Chart(data).to_dict()
      assert dct["data"]["values"] == data.to_dict(orient="records")
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = Chart(data).to_dict()
      name = dct["data"]["name"]
      assert dct["datasets"][name] == data.to_dict(orient="records")
  
      # Named data object
      data = alt.NamedData(name="Foo")
      dct = Chart(data).to_dict()
      assert dct["data"] == {"name": "Foo"}
  
  
  def test_chart_infer_types():
      data = pd.DataFrame(
          {
              "x": pd.date_range("2012", periods=10, freq="Y"),
              "y": range(10),
              "c": list("abcabcabca"),
          }
      )
  
      def _check_encodings(chart):
          dct = chart.to_dict()
          assert dct["encoding"]["x"]["type"] == "temporal"
          assert dct["encoding"]["x"]["field"] == "x"
          assert dct["encoding"]["y"]["type"] == "quantitative"
          assert dct["encoding"]["y"]["field"] == "y"
          assert dct["encoding"]["color"]["type"] == "nominal"
          assert dct["encoding"]["color"]["field"] == "c"
  
      # Pass field names by keyword
      chart = alt.Chart(data).mark_point().encode(x="x", y="y", color="c")
      _check_encodings(chart)
  
      # pass Channel objects by keyword
      chart = (
          alt.Chart(data)
          .mark_point()
          .encode(x=alt.X("x"), y=alt.Y("y"), color=alt.Color("c"))
      )
      _check_encodings(chart)
  
      # pass Channel objects by value
      chart = alt.Chart(data).mark_point().encode(alt.X("x"), alt.Y("y"), alt.Color("c"))
      _check_encodings(chart)
  
      # override default types
      chart = (
          alt.Chart(data)
          .mark_point()
          .encode(alt.X("x", type="nominal"), alt.Y("y", type="ordinal"))
      )
      dct = chart.to_dict()
      assert dct["encoding"]["x"]["type"] == "nominal"
      assert dct["encoding"]["y"]["type"] == "ordinal"
  
  
  @pytest.mark.parametrize(
      "args, kwargs",
      [
          getargs(detail=["value:Q", "name:N"], tooltip=["value:Q", "name:N"]),
          getargs(detail=["value", "name"], tooltip=["value", "name"]),
          getargs(alt.Detail(["value:Q", "name:N"]), alt.Tooltip(["value:Q", "name:N"])),
          getargs(alt.Detail(["value", "name"]), alt.Tooltip(["value", "name"])),
          getargs(
              [alt.Detail("value:Q"), alt.Detail("name:N")],
              [alt.Tooltip("value:Q"), alt.Tooltip("name:N")],
          ),
          getargs(
              [alt.Detail("value"), alt.Detail("name")],
              [alt.Tooltip("value"), alt.Tooltip("name")],
          ),
      ],
  )
  def test_multiple_encodings(args, kwargs):
      df = pd.DataFrame({"value": [1, 2, 3], "name": ["A", "B", "C"]})
      encoding_dct = [
          {"field": "value", "type": "quantitative"},
          {"field": "name", "type": "nominal"},
      ]
      chart = alt.Chart(df).mark_point().encode(*args, **kwargs)
      dct = chart.to_dict()
      assert dct["encoding"]["detail"] == encoding_dct
      assert dct["encoding"]["tooltip"] == encoding_dct
  
  
  def test_chart_operations():
      data = pd.DataFrame(
          {
              "x": pd.date_range("2012", periods=10, freq="Y"),
              "y": range(10),
              "c": list("abcabcabca"),
          }
      )
      chart1 = alt.Chart(data).mark_line().encode(x="x", y="y", color="c")
      chart2 = chart1.mark_point()
      chart3 = chart1.mark_circle()
      chart4 = chart1.mark_square()
  
      chart = chart1 + chart2 + chart3
      assert isinstance(chart, alt.LayerChart)
      assert len(chart.layer) == 3
      chart += chart4
      assert len(chart.layer) == 4
  
      chart = chart1 | chart2 | chart3
      assert isinstance(chart, alt.HConcatChart)
      assert len(chart.hconcat) == 3
      chart |= chart4
      assert len(chart.hconcat) == 4
  
      chart = chart1 & chart2 & chart3
      assert isinstance(chart, alt.VConcatChart)
      assert len(chart.vconcat) == 3
      chart &= chart4
      assert len(chart.vconcat) == 4
  
  
  def test_selection_to_dict():
      brush = alt.selection(type="interval")
  
      # test some value selections
      # Note: X and Y cannot have conditions
      alt.Chart("path/to/data.json").mark_point().encode(
          color=alt.condition(brush, alt.ColorValue("red"), alt.ColorValue("blue")),
          opacity=alt.condition(brush, alt.value(0.5), alt.value(1.0)),
          text=alt.condition(brush, alt.TextValue("foo"), alt.value("bar")),
      ).to_dict()
  
      # test some field selections
      # Note: X and Y cannot have conditions
      # Conditions cannot both be fields
      alt.Chart("path/to/data.json").mark_point().encode(
          color=alt.condition(brush, alt.Color("col1:N"), alt.value("blue")),
          opacity=alt.condition(brush, "col1:N", alt.value(0.5)),
          text=alt.condition(brush, alt.value("abc"), alt.Text("col2:N")),
          size=alt.condition(brush, alt.value(20), "col2:N"),
      ).to_dict()
  
  
  def test_selection_expression():
      selection = alt.selection_single(fields=["value"])
  
      assert isinstance(selection.value, alt.expr.Expression)
      assert selection.value.to_dict() == "{0}.value".format(selection.name)
  
      assert isinstance(selection["value"], alt.expr.Expression)
      assert selection["value"].to_dict() == "{0}['value']".format(selection.name)
  
      with pytest.raises(AttributeError):
          selection.__magic__
  
  
  @pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf"])
  def test_save(format, basic_chart):
      if format in ["pdf", "png"]:
          out = io.BytesIO()
          mode = "rb"
      else:
          out = io.StringIO()
          mode = "r"
  
      if format in ["svg", "png", "pdf"]:
          if not altair_saver:
              with pytest.raises(ValueError) as err:
                  basic_chart.save(out, format=format)
              assert "github.com/altair-viz/altair_saver" in str(err.value)
              return
          elif format not in altair_saver.available_formats():
              with pytest.raises(ValueError) as err:
                  basic_chart.save(out, format=format)
              assert f"No enabled saver found that supports format='{format}'" in str(
                  err.value
              )
              return
  
      basic_chart.save(out, format=format)
      out.seek(0)
      content = out.read()
  
      if format == "json":
          assert "$schema" in json.loads(content)
      if format == "html":
          assert content.startswith("<!DOCTYPE html>")
  
      fid, filename = tempfile.mkstemp(suffix="." + format)
      os.close(fid)
  
      # test both string filenames and pathlib.Paths
      for fp in [filename, pathlib.Path(filename)]:
          try:
              basic_chart.save(fp)
              with open(fp, mode) as f:
                  assert f.read()[:1000] == content[:1000]
          finally:
              os.remove(fp)
  
  
  def test_facet_basic():
      # wrapped facet
      chart1 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x="x:Q",
              y="y:Q",
          )
          .facet("category:N", columns=2)
      )
  
      dct1 = chart1.to_dict()
  
      assert dct1["facet"] == alt.Facet("category:N").to_dict()
      assert dct1["columns"] == 2
      assert dct1["data"] == alt.UrlData("data.csv").to_dict()
  
      # explicit row/col facet
      chart2 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x="x:Q",
              y="y:Q",
          )
          .facet(row="category1:Q", column="category2:Q")
      )
  
      dct2 = chart2.to_dict()
  
      assert dct2["facet"]["row"] == alt.Facet("category1:Q").to_dict()
      assert dct2["facet"]["column"] == alt.Facet("category2:Q").to_dict()
      assert "columns" not in dct2
      assert dct2["data"] == alt.UrlData("data.csv").to_dict()
  
  
  def test_facet_parse():
      chart = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(x="x:Q", y="y:Q")
          .facet(row="row:N", column="column:O")
      )
      dct = chart.to_dict()
      assert dct["data"] == {"url": "data.csv"}
      assert "data" not in dct["spec"]
      assert dct["facet"] == {
          "column": {"field": "column", "type": "ordinal"},
          "row": {"field": "row", "type": "nominal"},
      }
  
  
  def test_facet_parse_data():
      data = pd.DataFrame({"x": range(5), "y": range(5), "row": list("abcab")})
      chart = (
          alt.Chart(data)
          .mark_point()
          .encode(x="x", y="y:O")
          .facet(row="row", column="column:O")
      )
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = chart.to_dict()
      assert "values" in dct["data"]
      assert "data" not in dct["spec"]
      assert dct["facet"] == {
          "column": {"field": "column", "type": "ordinal"},
          "row": {"field": "row", "type": "nominal"},
      }
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = chart.to_dict()
      assert "datasets" in dct
      assert "name" in dct["data"]
      assert "data" not in dct["spec"]
      assert dct["facet"] == {
          "column": {"field": "column", "type": "ordinal"},
          "row": {"field": "row", "type": "nominal"},
      }
  
  
  def test_selection():
      # test instantiation of selections
      interval = alt.selection_interval(name="selec_1")
      assert interval.selection.type == "interval"
      assert interval.name == "selec_1"
  
      single = alt.selection_single(name="selec_2")
      assert single.selection.type == "single"
      assert single.name == "selec_2"
  
      multi = alt.selection_multi(name="selec_3")
      assert multi.selection.type == "multi"
      assert multi.name == "selec_3"
  
      # test adding to chart
      chart = alt.Chart().add_selection(single)
      chart = chart.add_selection(multi, interval)
      assert set(chart.selection.keys()) == {"selec_1", "selec_2", "selec_3"}
  
      # test logical operations
      assert isinstance(single & multi, alt.Selection)
      assert isinstance(single | multi, alt.Selection)
      assert isinstance(~single, alt.Selection)
      assert isinstance((single & multi)[0].group, alt.SelectionAnd)
      assert isinstance((single | multi)[0].group, alt.SelectionOr)
      assert isinstance((~single)[0].group, alt.SelectionNot)
  
      # test that default names increment (regression for #1454)
      sel1 = alt.selection_single()
      sel2 = alt.selection_multi()
      sel3 = alt.selection_interval()
      names = {s.name for s in (sel1, sel2, sel3)}
      assert len(names) == 3
  
  
  def test_transforms():
      # aggregate transform
      agg1 = alt.AggregatedFieldDef(**{"as": "x1", "op": "mean", "field": "y"})
      agg2 = alt.AggregatedFieldDef(**{"as": "x2", "op": "median", "field": "z"})
      chart = alt.Chart().transform_aggregate([agg1], ["foo"], x2="median(z)")
      kwds = dict(aggregate=[agg1, agg2], groupby=["foo"])
      assert chart.transform == [alt.AggregateTransform(**kwds)]
  
      # bin transform
      chart = alt.Chart().transform_bin("binned", field="field", bin=True)
      kwds = {"as": "binned", "field": "field", "bin": True}
      assert chart.transform == [alt.BinTransform(**kwds)]
  
      # calcualte transform
      chart = alt.Chart().transform_calculate("calc", "datum.a * 4")
      kwds = {"as": "calc", "calculate": "datum.a * 4"}
      assert chart.transform == [alt.CalculateTransform(**kwds)]
  
      # density transform
      chart = alt.Chart().transform_density("x", as_=["value", "density"])
      kwds = {"as": ["value", "density"], "density": "x"}
      assert chart.transform == [alt.DensityTransform(**kwds)]
  
      # filter transform
      chart = alt.Chart().transform_filter("datum.a < 4")
      assert chart.transform == [alt.FilterTransform(filter="datum.a < 4")]
  
      # flatten transform
      chart = alt.Chart().transform_flatten(["A", "B"], ["X", "Y"])
      kwds = {"as": ["X", "Y"], "flatten": ["A", "B"]}
      assert chart.transform == [alt.FlattenTransform(**kwds)]
  
      # fold transform
      chart = alt.Chart().transform_fold(["A", "B", "C"], as_=["key", "val"])
      kwds = {"as": ["key", "val"], "fold": ["A", "B", "C"]}
      assert chart.transform == [alt.FoldTransform(**kwds)]
  
      # impute transform
      chart = alt.Chart().transform_impute("field", "key", groupby=["x"])
      kwds = {"impute": "field", "key": "key", "groupby": ["x"]}
      assert chart.transform == [alt.ImputeTransform(**kwds)]
  
      # joinaggregate transform
      chart = alt.Chart().transform_joinaggregate(min="min(x)", groupby=["key"])
      kwds = {
          "joinaggregate": [
              alt.JoinAggregateFieldDef(field="x", op="min", **{"as": "min"})
          ],
          "groupby": ["key"],
      }
      assert chart.transform == [alt.JoinAggregateTransform(**kwds)]
  
      # loess transform
      chart = alt.Chart().transform_loess("x", "y", as_=["xx", "yy"])
      kwds = {"on": "x", "loess": "y", "as": ["xx", "yy"]}
      assert chart.transform == [alt.LoessTransform(**kwds)]
  
      # lookup transform (data)
      lookup_data = alt.LookupData(alt.UrlData("foo.csv"), "id", ["rate"])
      chart = alt.Chart().transform_lookup("a", from_=lookup_data, as_="a", default="b")
      kwds = {"from": lookup_data, "as": "a", "lookup": "a", "default": "b"}
      assert chart.transform == [alt.LookupTransform(**kwds)]
  
      # lookup transform (selection)
      lookup_selection = alt.LookupSelection(key="key", selection="sel")
      chart = alt.Chart().transform_lookup(
          "a", from_=lookup_selection, as_="a", default="b"
      )
      kwds = {"from": lookup_selection, "as": "a", "lookup": "a", "default": "b"}
      assert chart.transform == [alt.LookupTransform(**kwds)]
  
      # pivot transform
      chart = alt.Chart().transform_pivot("x", "y")
      assert chart.transform == [alt.PivotTransform(pivot="x", value="y")]
  
      # quantile transform
      chart = alt.Chart().transform_quantile("x", as_=["prob", "value"])
      kwds = {"quantile": "x", "as": ["prob", "value"]}
      assert chart.transform == [alt.QuantileTransform(**kwds)]
  
      # regression transform
      chart = alt.Chart().transform_regression("x", "y", as_=["xx", "yy"])
      kwds = {"on": "x", "regression": "y", "as": ["xx", "yy"]}
      assert chart.transform == [alt.RegressionTransform(**kwds)]
  
      # sample transform
      chart = alt.Chart().transform_sample()
      assert chart.transform == [alt.SampleTransform(1000)]
  
      # stack transform
      chart = alt.Chart().transform_stack("stacked", "x", groupby=["y"])
      assert chart.transform == [
          alt.StackTransform(stack="x", groupby=["y"], **{"as": "stacked"})
      ]
  
      # timeUnit transform
      chart = alt.Chart().transform_timeunit("foo", field="x", timeUnit="date")
      kwds = {"as": "foo", "field": "x", "timeUnit": "date"}
      assert chart.transform == [alt.TimeUnitTransform(**kwds)]
  
      # window transform
      chart = alt.Chart().transform_window(xsum="sum(x)", ymin="min(y)", frame=[None, 0])
      window = [
          alt.WindowFieldDef(**{"as": "xsum", "field": "x", "op": "sum"}),
          alt.WindowFieldDef(**{"as": "ymin", "field": "y", "op": "min"}),
      ]
  
      # kwargs don't maintain order in Python < 3.6, so window list can
      # be reversed
      assert chart.transform == [
          alt.WindowTransform(frame=[None, 0], window=window)
      ] or chart.transform == [alt.WindowTransform(frame=[None, 0], window=window[::-1])]
  
  
  def test_filter_transform_selection_predicates():
      selector1 = alt.selection_interval(name="s1")
      selector2 = alt.selection_interval(name="s2")
      base = alt.Chart("data.txt").mark_point()
  
      chart = base.transform_filter(selector1)
      assert chart.to_dict()["transform"] == [{"filter": {"selection": "s1"}}]
  
      chart = base.transform_filter(~selector1)
      assert chart.to_dict()["transform"] == [{"filter": {"selection": {"not": "s1"}}}]
  
      chart = base.transform_filter(selector1 & selector2)
      assert chart.to_dict()["transform"] == [
          {"filter": {"selection": {"and": ["s1", "s2"]}}}
      ]
  
      chart = base.transform_filter(selector1 | selector2)
      assert chart.to_dict()["transform"] == [
          {"filter": {"selection": {"or": ["s1", "s2"]}}}
      ]
  
      chart = base.transform_filter(selector1 | ~selector2)
      assert chart.to_dict()["transform"] == [
          {"filter": {"selection": {"or": ["s1", {"not": "s2"}]}}}
      ]
  
      chart = base.transform_filter(~selector1 | ~selector2)
      assert chart.to_dict()["transform"] == [
          {"filter": {"selection": {"or": [{"not": "s1"}, {"not": "s2"}]}}}
      ]
  
      chart = base.transform_filter(~(selector1 & selector2))
      assert chart.to_dict()["transform"] == [
          {"filter": {"selection": {"not": {"and": ["s1", "s2"]}}}}
      ]
  
  
  def test_resolve_methods():
      chart = alt.LayerChart().resolve_axis(x="shared", y="independent")
      assert chart.resolve == alt.Resolve(
          axis=alt.AxisResolveMap(x="shared", y="independent")
      )
  
      chart = alt.LayerChart().resolve_legend(color="shared", fill="independent")
      assert chart.resolve == alt.Resolve(
          legend=alt.LegendResolveMap(color="shared", fill="independent")
      )
  
      chart = alt.LayerChart().resolve_scale(x="shared", y="independent")
      assert chart.resolve == alt.Resolve(
          scale=alt.ScaleResolveMap(x="shared", y="independent")
      )
  
  
  def test_layer_encodings():
      chart = alt.LayerChart().encode(x="column:Q")
      assert chart.encoding.x == alt.X(shorthand="column:Q")
  
  
  def test_add_selection():
      selections = [
          alt.selection_interval(),
          alt.selection_single(),
          alt.selection_multi(),
      ]
      chart = (
          alt.Chart()
          .mark_point()
          .add_selection(selections[0])
          .add_selection(selections[1], selections[2])
      )
      expected = {s.name: s.selection for s in selections}
      assert chart.selection == expected
  
  
  def test_repeat_add_selections():
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = base.add_selection(selection).repeat(list("ABC"))
      chart2 = base.repeat(list("ABC")).add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  def test_facet_add_selections():
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = base.add_selection(selection).facet("val:Q")
      chart2 = base.facet("val:Q").add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  def test_layer_add_selection():
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = alt.layer(base.add_selection(selection), base)
      chart2 = alt.layer(base, base).add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  @pytest.mark.parametrize("charttype", [alt.concat, alt.hconcat, alt.vconcat])
  def test_compound_add_selections(charttype):
      base = alt.Chart("data.csv").mark_point()
      selection = alt.selection_single()
      chart1 = charttype(base.add_selection(selection), base.add_selection(selection))
      chart2 = charttype(base, base).add_selection(selection)
      assert chart1.to_dict() == chart2.to_dict()
  
  
  def test_selection_property():
      sel = alt.selection_interval()
      chart = alt.Chart("data.csv").mark_point().properties(selection=sel)
  
      assert list(chart["selection"].keys()) == [sel.name]
  
  
  def test_LookupData():
      df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
      lookup = alt.LookupData(data=df, key="x")
  
      dct = lookup.to_dict()
      assert dct["key"] == "x"
      assert dct["data"] == {
          "values": [{"x": 1, "y": 4}, {"x": 2, "y": 5}, {"x": 3, "y": 6}]
      }
  
  
  def test_themes():
      chart = alt.Chart("foo.txt").mark_point()
  
      with alt.themes.enable("default"):
          assert chart.to_dict()["config"] == {
              "view": {"continuousWidth": 400, "continuousHeight": 300}
          }
  
      with alt.themes.enable("opaque"):
          assert chart.to_dict()["config"] == {
              "background": "white",
              "view": {"continuousWidth": 400, "continuousHeight": 300},
          }
  
      with alt.themes.enable("none"):
          assert "config" not in chart.to_dict()
  
  
  def test_chart_from_dict():
      base = alt.Chart("data.csv").mark_point().encode(x="x:Q", y="y:Q")
  
      charts = [
          base,
          base + base,
          base | base,
          base & base,
          base.facet("c:N"),
          (base + base).facet(row="c:N", data="data.csv"),
          base.repeat(["c", "d"]),
          (base + base).repeat(row=["c", "d"]),
      ]
  
      for chart in charts:
          chart_out = alt.Chart.from_dict(chart.to_dict())
          assert type(chart_out) is type(chart)
  
      # test that an invalid spec leads to a schema validation error
      with pytest.raises(jsonschema.ValidationError):
          alt.Chart.from_dict({"invalid": "spec"})
  
  
  def test_consolidate_datasets(basic_chart):
      subchart1 = basic_chart
      subchart2 = basic_chart.copy()
      subchart2.data = basic_chart.data.copy()
      chart = subchart1 | subchart2
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct_consolidated = chart.to_dict()
  
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct_standard = chart.to_dict()
  
      assert "datasets" in dct_consolidated
      assert "datasets" not in dct_standard
  
      datasets = dct_consolidated["datasets"]
  
      # two dataset copies should be recognized as duplicates
      assert len(datasets) == 1
  
      # make sure data matches original & names are correct
      name, data = datasets.popitem()
  
      for spec in dct_standard["hconcat"]:
          assert spec["data"]["values"] == data
  
      for spec in dct_consolidated["hconcat"]:
          assert spec["data"] == {"name": name}
  
  
  def test_consolidate_InlineData():
      data = alt.InlineData(
          values=[{"a": 1, "b": 1}, {"a": 2, "b": 2}], format={"type": "csv"}
      )
      chart = alt.Chart(data).mark_point()
  
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = chart.to_dict()
      assert dct["data"]["format"] == data.format
      assert dct["data"]["values"] == data.values
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = chart.to_dict()
      assert dct["data"]["format"] == data.format
      assert list(dct["datasets"].values())[0] == data.values
  
      data = alt.InlineData(values=[], name="runtime_data")
      chart = alt.Chart(data).mark_point()
  
      with alt.data_transformers.enable(consolidate_datasets=False):
          dct = chart.to_dict()
      assert dct["data"] == data.to_dict()
  
      with alt.data_transformers.enable(consolidate_datasets=True):
          dct = chart.to_dict()
      assert dct["data"] == data.to_dict()
  
  
  def test_repeat():
      # wrapped repeat
      chart1 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x=alt.X(alt.repeat(), type="quantitative"),
              y="y:Q",
          )
          .repeat(["A", "B", "C", "D"], columns=2)
      )
  
      dct1 = chart1.to_dict()
  
      assert dct1["repeat"] == ["A", "B", "C", "D"]
      assert dct1["columns"] == 2
      assert dct1["spec"]["encoding"]["x"]["field"] == {"repeat": "repeat"}
  
      # explicit row/col repeat
      chart2 = (
          alt.Chart("data.csv")
          .mark_point()
          .encode(
              x=alt.X(alt.repeat("row"), type="quantitative"),
              y=alt.Y(alt.repeat("column"), type="quantitative"),
          )
          .repeat(row=["A", "B", "C"], column=["C", "B", "A"])
      )
  
      dct2 = chart2.to_dict()
  
      assert dct2["repeat"] == {"row": ["A", "B", "C"], "column": ["C", "B", "A"]}
      assert "columns" not in dct2
      assert dct2["spec"]["encoding"]["x"]["field"] == {"repeat": "row"}
      assert dct2["spec"]["encoding"]["y"]["field"] == {"repeat": "column"}
  
  
  def test_data_property():
      data = pd.DataFrame({"x": [1, 2, 3], "y": list("ABC")})
      chart1 = alt.Chart(data).mark_point()
      chart2 = alt.Chart().mark_point().properties(data=data)
  
      assert chart1.to_dict() == chart2.to_dict()
  
  
  @pytest.mark.parametrize("method", ["layer", "hconcat", "vconcat", "concat"])
  @pytest.mark.parametrize(
      "data", ["data.json", pd.DataFrame({"x": range(3), "y": list("abc")})]
  )
  def test_subcharts_with_same_data(method, data):
      func = getattr(alt, method)
  
      point = alt.Chart(data).mark_point().encode(x="x:Q", y="y:Q")
      line = point.mark_line()
      text = point.mark_text()
  
      chart1 = func(point, line, text)
      assert chart1.data is not alt.Undefined
      assert all(c.data is alt.Undefined for c in getattr(chart1, method))
  
      if method != "concat":
          op = OP_DICT[method]
          chart2 = op(op(point, line), text)
          assert chart2.data is not alt.Undefined
          assert all(c.data is alt.Undefined for c in getattr(chart2, method))
  
  
  @pytest.mark.parametrize("method", ["layer", "hconcat", "vconcat", "concat"])
  @pytest.mark.parametrize(
      "data", ["data.json", pd.DataFrame({"x": range(3), "y": list("abc")})]
  )
  def test_subcharts_different_data(method, data):
      func = getattr(alt, method)
  
      point = alt.Chart(data).mark_point().encode(x="x:Q", y="y:Q")
      otherdata = alt.Chart("data.csv").mark_point().encode(x="x:Q", y="y:Q")
      nodata = alt.Chart().mark_point().encode(x="x:Q", y="y:Q")
  
      chart1 = func(point, otherdata)
      assert chart1.data is alt.Undefined
      assert getattr(chart1, method)[0].data is data
  
      chart2 = func(point, nodata)
      assert chart2.data is alt.Undefined
      assert getattr(chart2, method)[0].data is data
  
  
  def test_layer_facet(basic_chart):
      chart = (basic_chart + basic_chart).facet(row="row:Q")
      assert chart.data is not alt.Undefined
      assert chart.spec.data is alt.Undefined
      for layer in chart.spec.layer:
          assert layer.data is alt.Undefined
  
      dct = chart.to_dict()
      assert "data" in dct
  
  
  def test_layer_errors():
      toplevel_chart = alt.Chart("data.txt").mark_point().configure_legend(columns=2)
  
      facet_chart1 = alt.Chart("data.txt").mark_point().encode(facet="row:Q")
  
      facet_chart2 = alt.Chart("data.txt").mark_point().facet("row:Q")
  
      repeat_chart = alt.Chart("data.txt").mark_point().repeat(["A", "B", "C"])
  
      simple_chart = alt.Chart("data.txt").mark_point()
  
      with pytest.raises(ValueError) as err:
          toplevel_chart + simple_chart
      assert str(err.value).startswith(
          'Objects with "config" attribute cannot be used within LayerChart.'
      )
  
      with pytest.raises(ValueError) as err:
          repeat_chart + simple_chart
      assert str(err.value) == "Repeat charts cannot be layered."
  
      with pytest.raises(ValueError) as err:
          facet_chart1 + simple_chart
      assert str(err.value) == "Faceted charts cannot be layered."
  
      with pytest.raises(ValueError) as err:
          alt.layer(simple_chart) + facet_chart2
      assert str(err.value) == "Faceted charts cannot be layered."
  
  
  @pytest.mark.parametrize(
      "chart_type",
      ["layer", "hconcat", "vconcat", "concat", "facet", "facet_encoding", "repeat"],
  )
  def test_resolve(chart_type):
      chart = _make_chart_type(chart_type)
      chart = (
          chart.resolve_scale(
              x="independent",
          )
          .resolve_legend(color="independent")
          .resolve_axis(y="independent")
      )
      dct = chart.to_dict()
      assert dct["resolve"] == {
          "scale": {"x": "independent"},
          "legend": {"color": "independent"},
          "axis": {"y": "independent"},
      }
  
  
  # TODO: test vconcat, hconcat, concat, facet_encoding when schema allows them.
  # This is blocked by https://github.com/vega/vega-lite/issues/5261
  @pytest.mark.parametrize("chart_type", ["chart", "layer"])
  @pytest.mark.parametrize("facet_arg", [None, "facet", "row", "column"])
  def test_facet(chart_type, facet_arg):
      chart = _make_chart_type(chart_type)
      if facet_arg is None:
          chart = chart.facet("color:N", columns=2)
      else:
          chart = chart.facet(**{facet_arg: "color:N", "columns": 2})
      dct = chart.to_dict()
  
      assert "spec" in dct
      assert dct["columns"] == 2
      expected = {"field": "color", "type": "nominal"}
      if facet_arg is None or facet_arg == "facet":
          assert dct["facet"] == expected
      else:
          assert dct["facet"][facet_arg] == expected
  
  
  def test_sequence():
      data = alt.sequence(100)
      assert data.to_dict() == {"sequence": {"start": 0, "stop": 100}}
  
      data = alt.sequence(5, 10)
      assert data.to_dict() == {"sequence": {"start": 5, "stop": 10}}
  
      data = alt.sequence(0, 1, 0.1, as_="x")
      assert data.to_dict() == {
          "sequence": {"start": 0, "stop": 1, "step": 0.1, "as": "x"}
      }
  
  
  def test_graticule():
      data = alt.graticule()
      assert data.to_dict() == {"graticule": True}
  
      data = alt.graticule(step=[15, 15])
      assert data.to_dict() == {"graticule": {"step": [15, 15]}}
  
  
  def test_sphere():
      data = alt.sphere()
      assert data.to_dict() == {"sphere": True}
  
  
  def test_validate_dataset():
      d = {"data": {"values": [{}]}, "mark": {"type": "point"}}
  
      chart = alt.Chart.from_dict(d)
      jsn = chart.to_json()
  
      assert jsn
  
files: altair/vegalite/v5/tests/test_api.py