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
  
  
  [... the rest is omitted ...]
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
  
  [... the rest is omitted ...]
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")]
  
      #
  
  [... the rest is omitted ...]
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")]
  
      #
  
  [... the rest is omitted ...]
files: altair/vegalite/v5/tests/test_api.py