Repository: altair-viz/altair
Pull request #2908 (2023-02-20 15:32:37)

Update parameter docstrings and signatures
Addresses #2692.

Adds more complete docstrings and signatures for parameter creation functions.  Also deprecates `selection` (in favor of `selection_point` and `selection_interval`).

Although this is primarily about documentation, the signatures were changed, so it is worth checking this carefully.  I did go through the examples [here](https://joelostblom.github.io/altair-docs/user_guide/interactions.html) and they seem to still be working.

This parameters portion of Altair has more customization and divergence from Vega-Lite (I gave one example in https://github.com/altair-viz/altair/issues/2692#issuecomment-1436026066), so these changes will not automatically be updated if changes happen in the Vega-Lite schema.  If anyone sees a more robust way to accomplish this, please let me know or feel free to make changes!

In the doc strings, I just approximated how long of lines we typically used in docstrings and inserted line breaks at corresponding locations.  I'm not sure if there is a way to have VS Code or something else automate that process, or if it's a problem that there is probably some inconsistency in terms of the exact lengths of the lines.

mattijn: Thanks @ChristopherDavisUCI! One short comment, I miss the dictating value `expr` in `param` (`alt.param(expr=...)`).
ChristopherDavisUCI: Thanks @mattijn, I added `expr` just now.  There aren't any `expr` examples here https://joelostblom.github.io/altair-docs/user_guide/interactions.html, do you think one should be included?

There were a few uncertainties like the following.  In https://vega.github.io/vega-lite/docs/parameter.html#expr the type of `expr` is listed as `Expr`, so I put expr : :class:`Expr` (optional) for consistency.  In practice, I think most people will use a string.  Would it be better to have expr : string or :class:`Expr` or maybe some other version?
joelostblom: Thanks for working on this! I think improved docstrings would be helpful here. Regarding expanding the docs for expressions I also agree that this is need and opened https://github.com/altair-viz/altair/issues/2904 yesterday with what I think would be ideal to have in an `expr` section in the docs.
binste: Great to have better docstrings and signatures! Just briefly, I saw that there are some __doc__ assignments which I'm not sure if they work in vs code for the same reasoning as we are discussing in the issue about ".encode". I'm on my phone now but could test this until Friday.

Patch:
diff --git a/altair/expr/__init__.py b/altair/expr/__init__.py
index 8390b992..41077e66 100644
--- a/altair/expr/__init__.py
+++ b/altair/expr/__init__.py
@@ -3,3 +3,15 @@
 from .core import datum, Expression
 from .funcs import *
 from .consts import *
+from ..vegalite.v5.schema.core import ExprRef as _ExprRef
+
+
+class _ExprType:
+    def __init__(self, expr):
+        vars(self).update(expr)
+
+    def __call__(self, expr, **kwargs):
+        return _ExprRef(expr, **kwargs)
+
+
+expr = _ExprType(globals())
diff --git a/altair/expr/core.py b/altair/expr/core.py
index 310a5ce9..9cc258c8 100644
--- a/altair/expr/core.py
+++ b/altair/expr/core.py
@@ -1,7 +1,7 @@
 from ..utils import SchemaBase
 
 
-class DatumType(object):
+class DatumType:
     """An object to assist in building Vega-Lite Expressions"""
 
     def __repr__(self):
@@ -38,7 +38,7 @@ def _js_repr(val):
 
 
 # Designed to work with Expression and VariableParameter
-class OperatorMixin(object):
+class OperatorMixin:
     def _to_expr(self):
         return repr(self)
 
diff --git a/altair/expr/funcs.py b/altair/expr/funcs.py
index ed86d536..f4a9118a 100644
--- a/altair/expr/funcs.py
+++ b/altair/expr/funcs.py
@@ -144,7 +144,7 @@ FUNCTION_LISTING = {
 NAME_MAP = {"if": "if_"}
 
 
-class ExprFunc(object):
+class ExprFunc:
     def __init__(self, name, doc):
         self.name = name
         self.doc = doc
diff --git a/altair/utils/core.py b/altair/utils/core.py
index 38bf0950..16581692 100644
--- a/altair/utils/core.py
+++ b/altair/utils/core.py
@@ -532,6 +532,23 @@ def parse_shorthand(
             if isinstance(attrs["type"], tuple):
                 attrs["sort"] = attrs["type"][1]
                 attrs["type"] = attrs["type"][0]
+
+    # If an unescaped colon is still present, it's often due to an incorrect data type specification
+    # but could also be due to using a column name with ":" in it.
+    if (
+        "field" in attrs
+        and ":" in attrs["field"]
+        and attrs["field"][attrs["field"].rfind(":") - 1] != "\\"
+    ):
+        raise ValueError(
+            '"{}" '.format(attrs["field"].split(":")[-1])
+            + "is not one of the valid encoding data types: {}.".format(
+                ", ".join(TYPECODE_MAP.values())
+            )
+            + "\nFor more details, see https://altair-viz.github.io/altair-docs/user_guide/encodings/index.html#encoding-data-types. "
+            + "If you are trying to use a column name that contains a colon, "
+            + 'prefix it with a backslash; for example "column\\:name" instead of "column:name".'
+        )
     return attrs
 
 
@@ -546,7 +563,11 @@ def use_signature(Obj):
 
         # Supplement the docstring of f with information from Obj
         if Obj.__doc__:
+            # Patch in a reference to the class this docstring is copied from,
+            # to generate a hyperlink.
             doclines = Obj.__doc__.splitlines()
+            doclines[0] = f"Refer to :class:`{Obj.__name__}`"
+
             if f.__doc__:
                 doc = f.__doc__ + "\n".join(doclines[1:])
             else:
diff --git a/altair/utils/deprecation.py b/altair/utils/deprecation.py
index 0230fda9..54e5159a 100644
--- a/altair/utils/deprecation.py
+++ b/altair/utils/deprecation.py
@@ -40,7 +40,7 @@ def _deprecate(obj, name=None, message=None):
 
     Examples
     --------
-    >>> class Foo(object): pass
+    >>> class Foo: pass
     >>> OldFoo = _deprecate(Foo, "OldFoo")
     >>> f = OldFoo()  # doctest: +SKIP
     AltairDeprecationWarning: alt.OldFoo is deprecated. Use alt.Foo instead.
diff --git a/altair/utils/display.py b/altair/utils/display.py
index 50c87b10..506d7fbf 100644
--- a/altair/utils/display.py
+++ b/altair/utils/display.py
@@ -97,7 +97,7 @@ class RendererRegistry(PluginRegistry[RendererType]):
 # ==============================================================================
 
 
-class Displayable(object):
+class Displayable:
     """A base display class for VegaLite v1/v2.
 
     This class takes a VegaLite v1/v2 spec and does the following:
@@ -165,7 +165,7 @@ def json_renderer_base(spec, str_repr, **options):
     )
 
 
-class HTMLRenderer(object):
+class HTMLRenderer:
     """Object to render charts as HTML, with a unique output div each time"""
 
     def __init__(self, output_div="altair-viz-{}", **kwargs):
diff --git a/altair/utils/execeval.py b/altair/utils/execeval.py
index c135fe4a..514f874c 100644
--- a/altair/utils/execeval.py
+++ b/altair/utils/execeval.py
@@ -10,7 +10,7 @@ else:
         return ast.Module(nodelist)
 
 
-class _CatchDisplay(object):
+class _CatchDisplay:
     """Class to temporarily catch sys.displayhook"""
 
     def __init__(self):
diff --git a/altair/utils/plugin_registry.py b/altair/utils/plugin_registry.py
index 4bfa12d4..5ba019ab 100644
--- a/altair/utils/plugin_registry.py
+++ b/altair/utils/plugin_registry.py
@@ -22,7 +22,7 @@ class NoSuchEntryPoint(Exception):
         return f"No {self.name!r} entry point found in group {self.group!r}"
 
 
-class PluginEnabler(object):
+class PluginEnabler:
     """Context manager for enabling plugins
 
     This object lets you use enable() as a context manager to
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index f2926eda..0229bcac 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -6,6 +6,7 @@ import inspect
 import json
 import textwrap
 from typing import Any, Sequence, List
+from itertools import zip_longest
 
 import jsonschema
 import jsonschema.exceptions
@@ -15,6 +16,7 @@ import pandas as pd
 
 from altair import vegalite
 
+
 # If DEBUG_MODE is True, then schema objects are converted to dict and
 # validated at creation time. This slows things down, particularly for
 # larger specs, but leads to much more useful tracebacks for the user.
@@ -158,6 +160,63 @@ class SchemaValidationError(jsonschema.ValidationError):
         self.obj = obj
         self._additional_errors = getattr(err, "_additional_errors", [])
 
+    @staticmethod
+    def _format_params_as_table(param_dict_keys):
+        """Format param names into a table so that they are easier to read"""
+        param_names, name_lengths = zip(
+            *[
+                (name, len(name))
+                for name in param_dict_keys
+                if name not in ["kwds", "self"]
+            ]
+        )
+        # Worst case scenario with the same longest param name in the same
+        # row for all columns
+        max_name_length = max(name_lengths)
+        max_column_width = 80
+        # Output a square table if not too big (since it is easier to read)
+        num_param_names = len(param_names)
+        square_columns = int(np.ceil(num_param_names**0.5))
+        columns = min(max_column_width // max_name_length, square_columns)
+
+        # Compute roughly equal column heights to evenly divide the param names
+        def split_into_equal_parts(n, p):
+            return [n // p + 1] * (n % p) + [n // p] * (p - n % p)
+
+        column_heights = split_into_equal_parts(num_param_names, columns)
+
+        # Section the param names into columns and compute their widths
+        param_names_columns = []
+        column_max_widths = []
+        last_end_idx = 0
+        for ch in column_heights:
+            param_names_columns.append(param_names[last_end_idx : last_end_idx + ch])
+            column_max_widths.append(
+                max([len(param_name) for param_name in param_names_columns[-1]])
+            )
+            last_end_idx = ch + last_end_idx
+
+        # Transpose the param name columns into rows to facilitate looping
+        param_names_rows = []
+        for li in zip_longest(*param_names_columns, fillvalue=""):
+            param_names_rows.append(li)
+        # Build the table as a string by iterating over and formatting the rows
+        param_names_table = ""
+        for param_names_row in param_names_rows:
+            for num, param_name in enumerate(param_names_row):
+                # Set column width based on the longest param in the column
+                max_name_length_column = column_max_widths[num]
+                column_pad = 3
+                param_names_table += "{:<{}}".format(
+                    param_name, max_name_length_column + column_pad
+                )
+                # Insert newlines and spacing after the last element in each row
+                if num == (len(param_names_row) - 1):
+                    param_names_table += "\n"
+                    # 16 is the indendation of the returned multiline string below
+                    param_names_table += " " * 16
+        return param_names_table
+
     def __str__(self):
         # Try to get the lowest class possible in the chart hierarchy so
         # it can be displayed in the error message. This should lead to more informative
@@ -174,29 +233,51 @@ class SchemaValidationError(jsonschema.ValidationError):
             # back on the class of the top-level object which created
             # the SchemaValidationError
             cls = self.obj.__class__
-        schema_path = ["{}.{}".format(cls.__module__, cls.__name__)]
-        schema_path.extend(self.schema_path)
-        schema_path = "->".join(
-            str(val)
-            for val in schema_path[:-1]
-            if val not in (0, "properties", "additionalProperties", "patternProperties")
-        )
-        message = self.message
-        if self._additional_errors:
-            message += "\n        " + "\n        ".join(
-                [e.message for e in self._additional_errors]
-            )
-        return """Invalid specification
 
-        {}, validating {!r}
+        # Output all existing parameters when an unknown parameter is specified
+        if self.validator == "additionalProperties":
+            param_dict_keys = inspect.signature(cls).parameters.keys()
+            param_names_table = self._format_params_as_table(param_dict_keys)
+
+            # `cleandoc` removes multiline string indentation in the output
+            return inspect.cleandoc(
+                """`{}` has no parameter named {!r}
+
+                Existing parameter names are:
+                {}
+                See the help for `{}` to read the full description of these parameters
+                """.format(
+                    cls.__name__,
+                    self.message.split("('")[-1].split("'")[0],
+                    param_names_table,
+                    cls.__name__,
+                )
+            )
+        # Use the default error message for all other cases than unknown parameter errors
+        else:
+            message = self.message
+            # Add a summary line when parameters are passed an invalid value
+            # For example: "'asdf' is an invalid value for `stack`
+            if self.absolute_path:
+                # The indentation here must match that of `cleandoc` below
+                message = f"""'{self.instance}' is an invalid value for `{self.absolute_path[-1]}`:
+
+                {message}"""
+
+            if self._additional_errors:
+                message += "\n                " + "\n                ".join(
+                    [e.message for e in self._additional_errors]
+                )
 
-        {}
-        """.format(
-            schema_path, self.validator, message
-        )
+            return inspect.cleandoc(
+                """{}
+                """.format(
+                    message
+                )
+            )
 
 
-class UndefinedType(object):
+class UndefinedType:
     """A singleton object for marking undefined parameters"""
 
     __instance = None
@@ -217,7 +298,7 @@ class UndefinedType(object):
 Undefined: Any = UndefinedType()
 
 
-class SchemaBase(object):
+class SchemaBase:
     """Base class for schema wrappers.
 
     Each derived class should set the _schema class attribute (and optionally
@@ -567,7 +648,7 @@ def _passthrough(*args, **kwds):
     return args[0] if args else kwds
 
 
-class _FromDict(object):
+class _FromDict:
     """Class used to construct SchemaBase class hierarchies from a dict
 
     The primary purpose of using this class is to be able to build a hash table
@@ -682,7 +763,7 @@ class _FromDict(object):
             return cls(dct)
 
 
-class _PropertySetter(object):
+class _PropertySetter:
     def __init__(self, prop, schema):
         self.prop = prop
         self.schema = schema
diff --git a/altair/utils/server.py b/altair/utils/server.py
index f26a3f25..f2dfc29e 100644
--- a/altair/utils/server.py
+++ b/altair/utils/server.py
@@ -23,7 +23,7 @@ You must interrupt the kernel to cancel this command.
 # Mock server used for testing
 
 
-class MockRequest(object):
+class MockRequest:
     def makefile(self, *args, **kwargs):
         return IO(b"GET /")
 
@@ -31,7 +31,7 @@ class MockRequest(object):
         pass
 
 
-class MockServer(object):
+class MockServer:
     def __init__(self, ip_port, Handler):
         Handler(MockRequest(), ip_port[0], self)
 
diff --git a/altair/vegalite/v5/__init__.py b/altair/vegalite/v5/__init__.py
index 90b76bf8..f436fb25 100644
--- a/altair/vegalite/v5/__init__.py
+++ b/altair/vegalite/v5/__init__.py
@@ -4,8 +4,7 @@ from .api import *
 
 from ...datasets import list_datasets, load_dataset
 
-from ... import expr
-from ...expr import datum
+from ...expr import datum, expr
 
 from .display import VegaLite, renderers
 
diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py
index 81fd1062..28c7487f 100644
--- a/altair/vegalite/v5/api.py
+++ b/altair/vegalite/v5/api.py
@@ -303,29 +303,49 @@ def value(value, **kwargs):
     return dict(value=value, **kwargs)
 
 
-def param(name=None, select=None, **kwds):
-    """Create a named parameter.
+def param(
+    name=None,
+    value=Undefined,
+    bind=Undefined,
+    empty=Undefined,
+    expr=Undefined,
+    **kwds,
+):
+    """Create a named parameter.  See https://altair-viz.github.io/user_guide/interactions.html for examples.  Although both variable parameters and selection parameters can be created using this 'param' function, to create a selection parameter, it is recommended to use either 'selection_point' or 'selection_interval' instead.
 
     Parameters
     ----------
     name : string (optional)
         The name of the parameter. If not specified, a unique name will be
         created.
+    bind : :class:`Binding` (optional)
+        Binds the parameter to an external input element such as a slider,
+        selection list or radio button group.
+    value : any (optional)
+        The default value of the parameter. If not specified, the parameter
+        will be created without a default value.
+    empty : boolean (optional)
+        For selection parameters, the predicate of empty selections returns
+        True by default. Override this behavior, by setting this property
+        'empty=False'.
+    expr : :class:`Expr` (optional)
+        An expression for the value of the parameter. This expression may
+        include other parameters, in which case the parameter will
+        automatically update in response to upstream parameter changes.
     **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.
+        is among the keywords, then a selection parameter will be created.
+        Otherwise, a variable parameter 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 empty is not Undefined:
+        parameter.empty = empty
         if parameter.empty == "none":
             warnings.warn(
                 """The value of 'empty' should be True or False.""",
@@ -348,50 +368,36 @@ def param(name=None, select=None, **kwds):
             """Use 'value' instead of 'init'.""",
             utils.AltairDeprecationWarning,
         )
-        if "value" not in kwds:
+        if value is Undefined:
             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)
+    if "select" not in kwds:
+        parameter.param = core.VariableParameter(
+            name=parameter.name, bind=bind, value=value, expr=expr, **kwds
+        )
         parameter.param_type = "variable"
     elif "views" in kwds:
         parameter.param = core.TopLevelSelectionParameter(
-            name=parameter.name, select=select, **kwds
+            name=parameter.name, bind=bind, value=value, expr=expr, **kwds
         )
         parameter.param_type = "selection"
     else:
         parameter.param = core.SelectionParameter(
-            name=parameter.name, select=select, **kwds
+            name=parameter.name, bind=bind, value=value, expr=expr, **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.
-    """
-
+def _selection(type=Undefined, **kwds):
     # We separate out the parameter keywords from the selection keywords
     param_kwds = {}
 
-    for kwd in {"name", "value", "bind", "empty", "init", "views"}:
+    for kwd in {"name", "bind", "value", "empty", "init", "views"}:
         if kwd in kwds:
             param_kwds[kwd] = kwds.pop(kwd)
 
@@ -412,16 +418,247 @@ def selection(type=Undefined, **kwds):
     return param(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)
+def selection(type=Undefined, **kwds):
+    """Create a selection parameter.  Users are recommended to use either 'selection_point' or 'selection_interval' instead, depending on the type of parameter they want to create.
 
+    Parameters
+    ----------
+    type : enum('point', 'interval') (required)
+        Determines the default event processing and data query for the
+        selection. Vega-Lite currently supports two selection types:
+        * "point" - to select multiple discrete data values; the first
+        value is selected on click and additional values toggled on
+        shift-click.
+        * "interval" - to select a continuous range of data values on
+        drag.
+    **kwds :
+        additional keywords to control the selection.
+    """
 
-@utils.use_signature(core.PointSelectionConfig)
-def selection_point(**kwargs):
-    """Create a selection with type='point'"""
-    return selection(type="point", **kwargs)
+    return _selection(type=type, **kwds)
+
+
+def selection_interval(
+    name=None,
+    value=Undefined,
+    bind=Undefined,
+    empty=Undefined,
+    expr=Undefined,
+    encodings=Undefined,
+    on=Undefined,
+    clear=Undefined,
+    resolve=Undefined,
+    mark=Undefined,
+    translate=Undefined,
+    zoom=Undefined,
+    **kwds,
+):
+    """Create a selection parameter with `type='interval'`.  Selection parameters define data queries that are driven by direct manipulation from user input (e.g., mouse clicks or drags). Selection parameters with `type='interval'` are used to select a continuous range of data values on drag.  (The current alternative is `type='point'`, which is used to select multiple discrete data values, and which is created using 'selection_point'.)
+
+    Parameters
+    ----------
+    name : string (optional)
+        The name of the parameter. If not specified, a unique name will be
+        created.
+    bind : :class:`Binding` (optional)
+        Binds the parameter to an external input element such as a slider,
+        selection list or radio button group.
+    value : any (optional)
+        The default value of the parameter. If not specified, the parameter
+        will be created without a default value.
+    empty : boolean (optional)
+        For selection parameters, the predicate of empty selections returns
+        True by default. Override this behavior, by setting this property
+        'empty=False'.
+    expr : :class:`Expr` (optional)
+        An expression for the value of the parameter. This expression may
+        include other parameters, in which case the parameter will
+        automatically update in response to upstream parameter changes.
+    encodings : List[str] (optional)
+        A list of encoding channels. The corresponding data field values
+        must match for a data tuple to fall within the selection.
+    fields : List[str] (optional)
+        A list of field names whose values must match for a data tuple to
+        fall within the selection.
+    on : string (optional)
+        A Vega event stream (object or selector) that triggers the selection.
+        For interval selections, the event stream must specify a start and end.
+    clear : string or boolean (optional)
+        Clears the selection, emptying it of all values. This property can
+        be an Event Stream or False to disable clear.  Default is 'dblclick'.
+    resolve : enum('global', 'union', 'intersect') (optional)
+        With layered and multi-view displays, a strategy that determines
+        how selections' data queries are resolved when applied in a filter
+        transform, conditional encoding rule, or scale domain.
+        One of:
+        * 'global' - only one brush exists for the entire SPLOM. When the
+            user begins to drag, any previous brushes are cleared, and a
+            new one is constructed.
+        * 'union' - each cell contains its own brush, and points are
+            highlighted if they lie within any of these individual brushes.
+        * 'intersect' - each cell contains its own brush, and points are
+            highlighted only if they fall within all of these individual
+            brushes.
+        The default is 'global'.
+    mark : :class:`Mark` (optional)
+        An interval selection also adds a rectangle mark to depict the
+        extents of the interval. The mark property can be used to
+        customize the appearance of the mark.
+    translate : string or boolean (optional)
+        When truthy, allows a user to interactively move an interval
+        selection back-and-forth. Can be True, False (to disable panning),
+        or a Vega event stream definition which must include a start and
+        end event to trigger continuous panning. Discrete panning (e.g.,
+        pressing the left/right arrow keys) will be supported in future
+        versions.
+        The default value is True, which corresponds to
+        [mousedown, window:mouseup] > window:mousemove!
+        This default allows users to click and drag within an interval
+        selection to reposition it.
+    zoom : string or boolean (optional)
+        When truthy, allows a user to interactively resize an interval
+        selection. Can be True, False (to disable zooming), or a Vega
+        event stream definition. Currently, only wheel events are supported,
+        but custom event streams can still be used to specify filters,
+        debouncing, and throttling. Future versions will expand the set of
+        events that can trigger this transformation.
+        The default value is True, which corresponds to wheel!. This
+        default allows users to use the mouse wheel to resize an interval
+        selection.
+    **kwds :
+        additional keywords to control the selection.
+
+    Returns
+    -------
+    parameter: Parameter
+        The parameter object that can be used in chart creation.
+    """
+    return _selection(
+        type="interval",
+        name=name,
+        value=value,
+        bind=bind,
+        empty=empty,
+        expr=expr,
+        encodings=encodings,
+        on=on,
+        clear=clear,
+        resolve=resolve,
+        mark=mark,
+        translate=translate,
+        zoom=zoom,
+        **kwds,
+    )
+
+
+def selection_point(
+    name=None,
+    value=Undefined,
+    bind=Undefined,
+    empty=Undefined,
+    expr=Undefined,
+    encodings=Undefined,
+    on=Undefined,
+    clear=Undefined,
+    resolve=Undefined,
+    toggle=Undefined,
+    nearest=Undefined,
+    **kwds,
+):
+    """Create a selection parameter with `type='point'`.  Selection parameters define data queries that are driven by direct manipulation from user input (e.g., mouse clicks or drags). Selection parameters with `type='point'` are used to select multiple discrete data values; the first value is selected on click and additional values toggled on shift-click. (The current alternative is `type='interval'`, which is used to select select a continuous range of data values on drag, and which is created using 'selection_interval'.)
+
+    Parameters
+    ----------
+    name : string (optional)
+        The name of the parameter. If not specified, a unique name will be
+        created.
+    bind : :class:`Binding` (optional)
+        Binds the parameter to an external input element such as a slider,
+        selection list or radio button group.
+    value : any (optional)
+        The default value of the parameter. If not specified, the parameter
+        will be created without a default value.
+    empty : boolean (optional)
+        For selection parameters, the predicate of empty selections returns
+        True by default. Override this behavior, by setting this property
+        'empty=False'.
+    expr : :class:`Expr` (optional)
+        An expression for the value of the parameter. This expression may
+        include other parameters, in which case the parameter will
+        automatically update in response to upstream parameter changes.
+    encodings : List[str] (optional)
+        A list of encoding channels. The corresponding data field values
+        must match for a data tuple to fall within the selection.
+    fields : List[str] (optional)
+        A list of field names whose values must match for a data tuple to
+        fall within the selection.
+    on : string (optional)
+        A Vega event stream (object or selector) that triggers the selection.
+        For interval selections, the event stream must specify a start and end.
+    clear : string or boolean (optional)
+        Clears the selection, emptying it of all values. This property can
+        be an Event Stream or False to disable clear.  Default is 'dblclick'.
+    resolve : enum('global', 'union', 'intersect') (optional)
+        With layered and multi-view displays, a strategy that determines
+        how selections' data queries are resolved when applied in a filter
+        transform, conditional encoding rule, or scale domain.
+        One of:
+        * 'global' - only one brush exists for the entire SPLOM. When the
+            user begins to drag, any previous brushes are cleared, and a
+            new one is constructed.
+        * 'union' - each cell contains its own brush, and points are
+            highlighted if they lie within any of these individual brushes.
+        * 'intersect' - each cell contains its own brush, and points are
+            highlighted only if they fall within all of these individual
+            brushes.
+        The default is 'global'.
+    toggle : string or boolean (optional)
+        Controls whether data values should be toggled (inserted or
+        removed from a point selection) or only ever inserted into
+        point selections.
+        One of:
+        * True - the default behavior, which corresponds to
+            "event.shiftKey". As a result, data values are toggled
+            when the user interacts with the shift-key pressed.
+        * False - disables toggling behaviour; the selection will
+            only ever contain a single data value corresponding
+            to the most recent interaction.
+        * A Vega expression which is re-evaluated as the user interacts.
+            If the expression evaluates to True, the data value is
+            toggled into or out of the point selection. If the expression
+            evaluates to False, the point selection is first cleared, and
+            the data value is then inserted. For example, setting the
+            value to the Vega expression True will toggle data values
+            without the user pressing the shift-key.
+    nearest : boolean (optional)
+        When true, an invisible voronoi diagram is computed to accelerate
+        discrete selection. The data value nearest the mouse cursor is
+        added to the selection.  The default is False, which means that
+        data values must be interacted with directly (e.g., clicked on)
+        to be added to the selection.
+    **kwds :
+        additional keywords to control the selection.
+
+    Returns
+    -------
+    parameter: Parameter
+        The parameter object that can be used in chart creation.
+    """
+    return _selection(
+        type="point",
+        name=name,
+        value=value,
+        bind=bind,
+        empty=empty,
+        expr=expr,
+        encodings=encodings,
+        on=on,
+        clear=clear,
+        resolve=resolve,
+        toggle=toggle,
+        nearest=nearest,
+        **kwds,
+    )
 
 
 @utils.deprecation.deprecated(
@@ -429,7 +666,7 @@ def selection_point(**kwargs):
 )
 @utils.use_signature(core.PointSelectionConfig)
 def selection_multi(**kwargs):
-    return selection(type="point", **kwargs)
+    return _selection(type="point", **kwargs)
 
 
 @utils.deprecation.deprecated(
@@ -437,7 +674,7 @@ def selection_multi(**kwargs):
 )
 @utils.use_signature(core.PointSelectionConfig)
 def selection_single(**kwargs):
-    return selection(type="point", **kwargs)
+    return _selection(type="point", **kwargs)
 
 
 @utils.use_signature(core.Binding)
@@ -634,7 +871,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         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)
+        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.
@@ -901,7 +1138,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         self: _TTopLevelMixin, aggregate=Undefined, groupby=Undefined, **kwds
     ) -> _TTopLevelMixin:
         """
-        Add an AggregateTransform to the schema.
+        Add an :class:`AggregateTransform` to the schema.
 
         Parameters
         ----------
@@ -977,7 +1214,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         self: _TTopLevelMixin, as_=Undefined, field=Undefined, bin=True, **kwargs
     ) -> _TTopLevelMixin:
         """
-        Add a BinTransform to the schema.
+        Add a :class:`BinTransform` to the schema.
 
         Parameters
         ----------
@@ -1035,7 +1272,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         self: _TTopLevelMixin, as_=Undefined, calculate=Undefined, **kwargs
     ) -> _TTopLevelMixin:
         """
-        Add a CalculateTransform to the schema.
+        Add a :class:`CalculateTransform` to the schema.
 
         Parameters
         ----------
@@ -1108,7 +1345,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         minsteps=Undefined,
         steps=Undefined,
     ) -> _TTopLevelMixin:
-        """Add a DensityTransform to the spec.
+        """Add a :class:`DensityTransform` to the spec.
 
         Parameters
         ----------
@@ -1174,7 +1411,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         value=Undefined,
     ) -> _TTopLevelMixin:
         """
-        Add an ImputeTransform to the schema.
+        Add an :class:`ImputeTransform` to the schema.
 
         Parameters
         ----------
@@ -1237,7 +1474,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         self: _TTopLevelMixin, joinaggregate=Undefined, groupby=Undefined, **kwargs
     ) -> _TTopLevelMixin:
         """
-        Add a JoinAggregateTransform to the schema.
+        Add a :class:`JoinAggregateTransform` to the schema.
 
         Parameters
         ----------
@@ -1288,7 +1525,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
     # TODO: Update docstring
     def transform_filter(self: _TTopLevelMixin, filter, **kwargs) -> _TTopLevelMixin:
         """
-        Add a FilterTransform to the schema.
+        Add a :class:`FilterTransform` to the schema.
 
         Parameters
         ----------
@@ -1322,7 +1559,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
     def transform_flatten(
         self: _TTopLevelMixin, flatten, as_=Undefined
     ) -> _TTopLevelMixin:
-        """Add a FlattenTransform to the schema.
+        """Add a :class:`FlattenTransform` to the schema.
 
         Parameters
         ----------
@@ -1350,7 +1587,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         )
 
     def transform_fold(self: _TTopLevelMixin, fold, as_=Undefined) -> _TTopLevelMixin:
-        """Add a FoldTransform to the spec.
+        """Add a :class:`FoldTransform` to the spec.
 
         Parameters
         ----------
@@ -1380,7 +1617,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         bandwidth=Undefined,
         groupby=Undefined,
     ) -> _TTopLevelMixin:
-        """Add a LoessTransform to the spec.
+        """Add a :class:`LoessTransform` to the spec.
 
         Parameters
         ----------
@@ -1422,7 +1659,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         default=Undefined,
         **kwargs,
     ) -> _TTopLevelMixin:
-        """Add a DataLookupTransform or SelectionLookupTransform to the chart
+        """Add a :class:`DataLookupTransform` or :class:`SelectionLookupTransform` to the chart
 
         Parameters
         ----------
@@ -1477,7 +1714,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         limit=Undefined,
         op=Undefined,
     ) -> _TTopLevelMixin:
-        """Add a pivot transform to the chart.
+        """Add a :class:`PivotTransform` to the chart.
 
         Parameters
         ----------
@@ -1523,7 +1760,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         probs=Undefined,
         step=Undefined,
     ) -> _TTopLevelMixin:
-        """Add a quantile transform to the chart
+        """Add a :class:`QuantileTransform` to the chart
 
         Parameters
         ----------
@@ -1572,7 +1809,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         order=Undefined,
         params=Undefined,
     ) -> _TTopLevelMixin:
-        """Add a RegressionTransform to the chart.
+        """Add a :class:`RegressionTransform` to the chart.
 
         Parameters
         ----------
@@ -1628,7 +1865,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
 
     def transform_sample(self: _TTopLevelMixin, sample=1000) -> _TTopLevelMixin:
         """
-        Add a SampleTransform to the schema.
+        Add a :class:`SampleTransform` to the schema.
 
         Parameters
         ----------
@@ -1650,7 +1887,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         self: _TTopLevelMixin, as_, stack, groupby, offset=Undefined, sort=Undefined
     ) -> _TTopLevelMixin:
         """
-        Add a StackTransform to the schema.
+        Add a :class:`StackTransform` to the schema.
 
         Parameters
         ----------
@@ -1691,7 +1928,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         **kwargs,
     ) -> _TTopLevelMixin:
         """
-        Add a TimeUnitTransform to the schema.
+        Add a :class:`TimeUnitTransform` to the schema.
 
         Parameters
         ----------
@@ -1776,7 +2013,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         sort=Undefined,
         **kwargs,
     ) -> _TTopLevelMixin:
-        """Add a WindowTransform to the schema
+        """Add a :class:`WindowTransform` to the schema
 
         Parameters
         ----------
@@ -2026,7 +2263,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         return self._set_resolve(scale=core.ScaleResolveMap(*args, **kwargs))
 
 
-class _EncodingMixin(object):
+class _EncodingMixin:
     @utils.use_signature(core.FacetedEncoding)
     def encode(self: _TEncodingMixin, *args, **kwargs) -> _TEncodingMixin:
         # Convert args to kwargs based on their types.
diff --git a/altair/vegalite/v5/schema/channels.py b/altair/vegalite/v5/schema/channels.py
index 13b5453f..5b5e82e1 100644
--- a/altair/vegalite/v5/schema/channels.py
+++ b/altair/vegalite/v5/schema/channels.py
@@ -8,7 +8,7 @@ from altair.utils import parse_shorthand
 from typing import overload, Type
 
 
-class FieldChannelMixin(object):
+class FieldChannelMixin:
     def to_dict(self, validate=True, ignore=(), context=None):
         context = context or {}
         shorthand = self._get('shorthand')
@@ -38,9 +38,12 @@ class FieldChannelMixin(object):
                 parsed.pop('type', None)
             elif not (type_in_shorthand or type_defined_explicitly):
                 if isinstance(context.get('data', None), pd.DataFrame):
-                    raise ValueError("{} encoding field is specified without a type; "
-                                     "the type cannot be inferred because it does not "
-                                     "match any column in the data.".format(shorthand))
+                    raise ValueError(
+                        'Unable to determine data type for the field "{}";'
+                        " verify that the field name is not misspelled."
+                        " If you are referencing a field from a transform,"
+                        " also confirm that the data type is specified correctly.".format(shorthand)
+                    )
                 else:
                     raise ValueError("{} encoding field is specified without a type; "
                                      "the type cannot be automatically inferred because "
@@ -59,7 +62,7 @@ class FieldChannelMixin(object):
         )
 
 
-class ValueChannelMixin(object):
+class ValueChannelMixin:
     def to_dict(self, validate=True, ignore=(), context=None):
         context = context or {}
         condition = self._get('condition', Undefined)
@@ -76,7 +79,7 @@ class ValueChannelMixin(object):
                                                       context=context)
 
 
-class DatumChannelMixin(object):
+class DatumChannelMixin:
     def to_dict(self, validate=True, ignore=(), context=None):
         context = context or {}
         datum = self._get('datum', Undefined)
diff --git a/altair/vegalite/v5/schema/mixins.py b/altair/vegalite/v5/schema/mixins.py
index 49cdd405..e386c29e 100644
--- a/altair/vegalite/v5/schema/mixins.py
+++ b/altair/vegalite/v5/schema/mixins.py
@@ -9,7 +9,7 @@ _TConfigMethodMixin = TypeVar("_TConfigMethodMixin", bound="ConfigMethodMixin")
 _TMarkMethodMixin = TypeVar("_TMarkMethodMixin", bound="MarkMethodMixin")
 
 
-class MarkMethodMixin(object):
+class MarkMethodMixin:
     """A mixin class that defines mark methods"""
 
     def mark_arc(self: _TMarkMethodMixin, align=Undefined, angle=Undefined, aria=Undefined,
@@ -36,9 +36,7 @@ class MarkMethodMixin(object):
                  url=Undefined, width=Undefined, x=Undefined, x2=Undefined, x2Offset=Undefined,
                  xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined, yOffset=Undefined,
                  **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'arc'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'arc' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -95,9 +93,7 @@ class MarkMethodMixin(object):
                   tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                   x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined,
                   yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'area'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'area' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -154,9 +150,7 @@ class MarkMethodMixin(object):
                  url=Undefined, width=Undefined, x=Undefined, x2=Undefined, x2Offset=Undefined,
                  xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined, yOffset=Undefined,
                  **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'bar'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'bar' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -213,9 +207,7 @@ class MarkMethodMixin(object):
                    timeUnitBandSize=Undefined, tooltip=Undefined, url=Undefined, width=Undefined,
                    x=Undefined, x2=Undefined, x2Offset=Undefined, xOffset=Undefined, y=Undefined,
                    y2=Undefined, y2Offset=Undefined, yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'image'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'image' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -272,9 +264,7 @@ class MarkMethodMixin(object):
                   tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                   x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined,
                   yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'line'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'line' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -331,9 +321,7 @@ class MarkMethodMixin(object):
                    timeUnitBandSize=Undefined, tooltip=Undefined, url=Undefined, width=Undefined,
                    x=Undefined, x2=Undefined, x2Offset=Undefined, xOffset=Undefined, y=Undefined,
                    y2=Undefined, y2Offset=Undefined, yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'point'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'point' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -390,9 +378,7 @@ class MarkMethodMixin(object):
                   tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                   x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined,
                   yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'rect'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'rect' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -449,9 +435,7 @@ class MarkMethodMixin(object):
                   tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                   x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined,
                   yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'rule'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'rule' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -508,9 +492,7 @@ class MarkMethodMixin(object):
                   tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                   x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined,
                   yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'text'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'text' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -567,9 +549,7 @@ class MarkMethodMixin(object):
                   tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                   x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined, y2Offset=Undefined,
                   yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'tick'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'tick' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -626,9 +606,7 @@ class MarkMethodMixin(object):
                    timeUnitBandSize=Undefined, tooltip=Undefined, url=Undefined, width=Undefined,
                    x=Undefined, x2=Undefined, x2Offset=Undefined, xOffset=Undefined, y=Undefined,
                    y2=Undefined, y2Offset=Undefined, yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'trail'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'trail' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -686,9 +664,7 @@ class MarkMethodMixin(object):
                     tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                     x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined,
                     y2Offset=Undefined, yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'circle'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'circle' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -746,9 +722,7 @@ class MarkMethodMixin(object):
                     tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                     x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined,
                     y2Offset=Undefined, yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'square'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'square' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -806,9 +780,7 @@ class MarkMethodMixin(object):
                       tooltip=Undefined, url=Undefined, width=Undefined, x=Undefined, x2=Undefined,
                       x2Offset=Undefined, xOffset=Undefined, y=Undefined, y2=Undefined,
                       y2Offset=Undefined, yOffset=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'geoshape'
-    
-        For information on additional arguments, see :class:`MarkDef`
+        """Set the chart's mark to 'geoshape' (see :class:`MarkDef`)
         """
         kwds = dict(align=align, angle=angle, aria=aria, ariaRole=ariaRole,
                     ariaRoleDescription=ariaRoleDescription, aspect=aspect, bandSize=bandSize,
@@ -844,9 +816,7 @@ class MarkMethodMixin(object):
     def mark_boxplot(self: _TMarkMethodMixin, box=Undefined, clip=Undefined, color=Undefined,
                      extent=Undefined, median=Undefined, opacity=Undefined, orient=Undefined,
                      outliers=Undefined, rule=Undefined, size=Undefined, ticks=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'boxplot'
-    
-        For information on additional arguments, see :class:`BoxPlotDef`
+        """Set the chart's mark to 'boxplot' (see :class:`BoxPlotDef`)
         """
         kwds = dict(box=box, clip=clip, color=color, extent=extent, median=median, opacity=opacity,
                     orient=orient, outliers=outliers, rule=rule, size=size, ticks=ticks, **kwds)
@@ -860,9 +830,7 @@ class MarkMethodMixin(object):
     def mark_errorbar(self: _TMarkMethodMixin, clip=Undefined, color=Undefined, extent=Undefined,
                       opacity=Undefined, orient=Undefined, rule=Undefined, size=Undefined,
                       thickness=Undefined, ticks=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'errorbar'
-    
-        For information on additional arguments, see :class:`ErrorBarDef`
+        """Set the chart's mark to 'errorbar' (see :class:`ErrorBarDef`)
         """
         kwds = dict(clip=clip, color=color, extent=extent, opacity=opacity, orient=orient, rule=rule,
                     size=size, thickness=thickness, ticks=ticks, **kwds)
@@ -876,9 +844,7 @@ class MarkMethodMixin(object):
     def mark_errorband(self: _TMarkMethodMixin, band=Undefined, borders=Undefined, clip=Undefined,
                        color=Undefined, extent=Undefined, interpolate=Undefined, opacity=Undefined,
                        orient=Undefined, tension=Undefined, **kwds) -> _TMarkMethodMixin:
-        """Set the chart's mark to 'errorband'
-    
-        For information on additional arguments, see :class:`ErrorBandDef`
+        """Set the chart's mark to 'errorband' (see :class:`ErrorBandDef`)
         """
         kwds = dict(band=band, borders=borders, clip=clip, color=color, extent=extent,
                     interpolate=interpolate, opacity=opacity, orient=orient, tension=tension, **kwds)
@@ -890,7 +856,7 @@ class MarkMethodMixin(object):
         return copy
 
 
-class ConfigMethodMixin(object):
+class ConfigMethodMixin:
     """A mixin class that defines config methods"""
 
     @use_signature(core.Config)
diff --git a/altair/vegalite/v5/theme.py b/altair/vegalite/v5/theme.py
index ba396cb3..7fb1e397 100644
--- a/altair/vegalite/v5/theme.py
+++ b/altair/vegalite/v5/theme.py
@@ -13,7 +13,7 @@ VEGA_THEMES = [
 ]
 
 
-class VegaTheme(object):
+class VegaTheme:
     """Implementation of a builtin vega theme."""
 
     def __init__(self, theme):
diff --git a/doc/releases/changes.rst b/doc/releases/changes.rst
index 8e3f25df..0c9a44b1 100644
--- a/doc/releases/changes.rst
+++ b/doc/releases/changes.rst
@@ -21,7 +21,7 @@ Enhancements
 - Saving charts with HTML inline is now supported without having altair_saver installed (#2807).
 - The documentation page has been revamped, both in terms of appearance and content.
 - More informative autocompletion by removing deprecated methods (#2814) and adding support for completion in method chains for editors that rely on type hints (e.g. VS Code) (#2846)
-- Improved error messages (#2842)
+- Substantially improved error handling. Both in terms of finding the more relevant error (#2842), and in terms of improving the formatting and clarity of the error messages (#2824, #2568).
 - Include experimental support for the DataFrame Interchange Protocol (through `__dataframe__` attribute). This requires `pyarrow>=11.0.0` (#2888).
 
 Grammar Changes
@@ -45,6 +45,7 @@ Bug Fixes
 Backward-Incompatible Changes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+- Colons in column names must now be escaped to remove any ambiguity with encoding types. You now need to write ``"column\:name"`` instead of ``"column:name"`` (#2824).
 - Removed the Vega (v5) wrappers and deprecate rendering in Vega mode (save Chart as Vega format is still allowed) (#2829).
 - Removed the Vega-Lite 3 and 4 wrappers (#2847).
 - In regards to the grammar changes listed above, the old terminology will still work in many basic cases.  On the other hand, if that old terminology gets used at a lower level, then it most likely will not work.  For example, in the current version of :ref:`gallery_scatter_with_minimap`, two instances of the key ``param`` are used in dictionaries to specify axis domains.  Those used to be ``selection``, but that usage is not compatible with the current Vega-Lite schema.
diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py
index aacec676..4da0094e 100644
--- a/tools/generate_schema_wrapper.py
+++ b/tools/generate_schema_wrapper.py
@@ -62,7 +62,7 @@ def load_schema():
 
 
 CHANNEL_MIXINS = """
-class FieldChannelMixin(object):
+class FieldChannelMixin:
     def to_dict(self, validate=True, ignore=(), context=None):
         context = context or {}
         shorthand = self._get('shorthand')
@@ -92,9 +92,12 @@ class FieldChannelMixin(object):
                 parsed.pop('type', None)
             elif not (type_in_shorthand or type_defined_explicitly):
                 if isinstance(context.get('data', None), pd.DataFrame):
-                    raise ValueError("{} encoding field is specified without a type; "
-                                     "the type cannot be inferred because it does not "
-                                     "match any column in the data.".format(shorthand))
+                    raise ValueError(
+                        'Unable to determine data type for the field "{}";'
+                        " verify that the field name is not misspelled."
+                        " If you are referencing a field from a transform,"
+                        " also confirm that the data type is specified correctly.".format(shorthand)
+                    )
                 else:
                     raise ValueError("{} encoding field is specified without a type; "
                                      "the type cannot be automatically inferred because "
@@ -113,7 +116,7 @@ class FieldChannelMixin(object):
         )
 
 
-class ValueChannelMixin(object):
+class ValueChannelMixin:
     def to_dict(self, validate=True, ignore=(), context=None):
         context = context or {}
         condition = self._get('condition', Undefined)
@@ -130,7 +133,7 @@ class ValueChannelMixin(object):
                                                       context=context)
 
 
-class DatumChannelMixin(object):
+class DatumChannelMixin:
     def to_dict(self, validate=True, ignore=(), context=None):
         context = context or {}
         datum = self._get('datum', Undefined)
@@ -145,9 +148,7 @@ class DatumChannelMixin(object):
 
 MARK_METHOD = '''
 def mark_{mark}({def_arglist}) -> {self_type}:
-    """Set the chart's mark to '{mark}'
-
-    For information on additional arguments, see :class:`{mark_def}`
+    """Set the chart's mark to '{mark}' (see :class:`{mark_def}`)
     """
     kwds = dict({dict_arglist})
     copy = self.copy(deep=False)
@@ -513,7 +514,7 @@ def generate_vegalite_mark_mixin(schemafile, markdefs):
     )
 
     code = [
-        f"class {class_name}(object):",
+        f"class {class_name}:",
         '    """A mixin class that defines mark methods"""',
     ]
 
@@ -562,7 +563,7 @@ def generate_vegalite_config_mixin(schemafile):
     )
 
     code = [
-        f"class {class_name}(object):",
+        f"class {class_name}:",
         '    """A mixin class that defines config methods"""',
     ]
     with open(schemafile, encoding="utf8") as f:
diff --git a/tools/schemapi/codegen.py b/tools/schemapi/codegen.py
index e54bccfa..5292b20c 100644
--- a/tools/schemapi/codegen.py
+++ b/tools/schemapi/codegen.py
@@ -5,7 +5,7 @@ import textwrap
 import re
 
 
-class CodeSnippet(object):
+class CodeSnippet:
     """Object whose repr() is a string of code"""
 
     def __init__(self, code):
@@ -56,7 +56,7 @@ def _get_args(info):
     return (nonkeyword, required, kwds, invalid_kwds, additional)
 
 
-class SchemaGenerator(object):
+class SchemaGenerator:
     """Class that defines methods for generating code from schemas
 
     Parameters
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index 3d60fb80..b5da8e22 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -4,6 +4,7 @@ import inspect
 import json
 import textwrap
 from typing import Any, Sequence, List
+from itertools import zip_longest
 
 import jsonschema
 import jsonschema.exceptions
@@ -13,6 +14,7 @@ import pandas as pd
 
 from altair import vegalite
 
+
 # If DEBUG_MODE is True, then schema objects are converted to dict and
 # validated at creation time. This slows things down, particularly for
 # larger specs, but leads to much more useful tracebacks for the user.
@@ -156,6 +158,63 @@ class SchemaValidationError(jsonschema.ValidationError):
         self.obj = obj
         self._additional_errors = getattr(err, "_additional_errors", [])
 
+    @staticmethod
+    def _format_params_as_table(param_dict_keys):
+        """Format param names into a table so that they are easier to read"""
+        param_names, name_lengths = zip(
+            *[
+                (name, len(name))
+                for name in param_dict_keys
+                if name not in ["kwds", "self"]
+            ]
+        )
+        # Worst case scenario with the same longest param name in the same
+        # row for all columns
+        max_name_length = max(name_lengths)
+        max_column_width = 80
+        # Output a square table if not too big (since it is easier to read)
+        num_param_names = len(param_names)
+        square_columns = int(np.ceil(num_param_names**0.5))
+        columns = min(max_column_width // max_name_length, square_columns)
+
+        # Compute roughly equal column heights to evenly divide the param names
+        def split_into_equal_parts(n, p):
+            return [n // p + 1] * (n % p) + [n // p] * (p - n % p)
+
+        column_heights = split_into_equal_parts(num_param_names, columns)
+
+        # Section the param names into columns and compute their widths
+        param_names_columns = []
+        column_max_widths = []
+        last_end_idx = 0
+        for ch in column_heights:
+            param_names_columns.append(param_names[last_end_idx : last_end_idx + ch])
+            column_max_widths.append(
+                max([len(param_name) for param_name in param_names_columns[-1]])
+            )
+            last_end_idx = ch + last_end_idx
+
+        # Transpose the param name columns into rows to facilitate looping
+        param_names_rows = []
+        for li in zip_longest(*param_names_columns, fillvalue=""):
+            param_names_rows.append(li)
+        # Build the table as a string by iterating over and formatting the rows
+        param_names_table = ""
+        for param_names_row in param_names_rows:
+            for num, param_name in enumerate(param_names_row):
+                # Set column width based on the longest param in the column
+                max_name_length_column = column_max_widths[num]
+                column_pad = 3
+                param_names_table += "{:<{}}".format(
+                    param_name, max_name_length_column + column_pad
+                )
+                # Insert newlines and spacing after the last element in each row
+                if num == (len(param_names_row) - 1):
+                    param_names_table += "\n"
+                    # 16 is the indendation of the returned multiline string below
+                    param_names_table += " " * 16
+        return param_names_table
+
     def __str__(self):
         # Try to get the lowest class possible in the chart hierarchy so
         # it can be displayed in the error message. This should lead to more informative
@@ -172,29 +231,51 @@ class SchemaValidationError(jsonschema.ValidationError):
             # back on the class of the top-level object which created
             # the SchemaValidationError
             cls = self.obj.__class__
-        schema_path = ["{}.{}".format(cls.__module__, cls.__name__)]
-        schema_path.extend(self.schema_path)
-        schema_path = "->".join(
-            str(val)
-            for val in schema_path[:-1]
-            if val not in (0, "properties", "additionalProperties", "patternProperties")
-        )
-        message = self.message
-        if self._additional_errors:
-            message += "\n        " + "\n        ".join(
-                [e.message for e in self._additional_errors]
-            )
-        return """Invalid specification
 
-        {}, validating {!r}
+        # Output all existing parameters when an unknown parameter is specified
+        if self.validator == "additionalProperties":
+            param_dict_keys = inspect.signature(cls).parameters.keys()
+            param_names_table = self._format_params_as_table(param_dict_keys)
+
+            # `cleandoc` removes multiline string indentation in the output
+            return inspect.cleandoc(
+                """`{}` has no parameter named {!r}
+
+                Existing parameter names are:
+                {}
+                See the help for `{}` to read the full description of these parameters
+                """.format(
+                    cls.__name__,
+                    self.message.split("('")[-1].split("'")[0],
+                    param_names_table,
+                    cls.__name__,
+                )
+            )
+        # Use the default error message for all other cases than unknown parameter errors
+        else:
+            message = self.message
+            # Add a summary line when parameters are passed an invalid value
+            # For example: "'asdf' is an invalid value for `stack`
+            if self.absolute_path:
+                # The indentation here must match that of `cleandoc` below
+                message = f"""'{self.instance}' is an invalid value for `{self.absolute_path[-1]}`:
+
+                {message}"""
+
+            if self._additional_errors:
+                message += "\n                " + "\n                ".join(
+                    [e.message for e in self._additional_errors]
+                )
 
-        {}
-        """.format(
-            schema_path, self.validator, message
-        )
+            return inspect.cleandoc(
+                """{}
+                """.format(
+                    message
+                )
+            )
 
 
-class UndefinedType(object):
+class UndefinedType:
     """A singleton object for marking undefined parameters"""
 
     __instance = None
@@ -215,7 +296,7 @@ class UndefinedType(object):
 Undefined: Any = UndefinedType()
 
 
-class SchemaBase(object):
+class SchemaBase:
     """Base class for schema wrappers.
 
     Each derived class should set the _schema class attribute (and optionally
@@ -565,7 +646,7 @@ def _passthrough(*args, **kwds):
     return args[0] if args else kwds
 
 
-class _FromDict(object):
+class _FromDict:
     """Class used to construct SchemaBase class hierarchies from a dict
 
     The primary purpose of using this class is to be able to build a hash table
@@ -680,7 +761,7 @@ class _FromDict(object):
             return cls(dct)
 
 
-class _PropertySetter(object):
+class _PropertySetter:
     def __init__(self, prop, schema):
         self.prop = prop
         self.schema = schema
diff --git a/tools/schemapi/utils.py b/tools/schemapi/utils.py
index abc9d6ac..b7673177 100644
--- a/tools/schemapi/utils.py
+++ b/tools/schemapi/utils.py
@@ -94,7 +94,7 @@ def is_valid_identifier(var, allow_unicode=False):
     return is_valid and not keyword.iskeyword(var)
 
 
-class SchemaProperties(object):
+class SchemaProperties:
     """A wrapper for properties within a schema"""
 
     def __init__(self, properties, schema, rootschema=None):
@@ -133,7 +133,7 @@ class SchemaProperties(object):
         return (self[key] for key in self)
 
 
-class SchemaInfo(object):
+class SchemaInfo:
     """A wrapper for inspecting a JSON schema"""
 
     def __init__(self, schema, rootschema=None):


Test patch:
diff --git a/tests/expr/tests/test_expr.py b/tests/expr/tests/test_expr.py
index 204ff2c1..f57a1f3c 100644
--- a/tests/expr/tests/test_expr.py
+++ b/tests/expr/tests/test_expr.py
@@ -4,6 +4,8 @@ import pytest
 
 from altair import expr
 from altair import datum
+from altair import ExprRef
+from jsonschema.exceptions import ValidationError
 
 
 def test_unary_operations():
@@ -104,3 +106,27 @@ def test_datum_getattr():
 def test_expression_getitem():
     x = datum.foo[0]
     assert repr(x) == "datum.foo[0]"
+
+
+def test_expression_function_expr():
+    # test including a expr.<CONSTANT> should return an ExprRef
+    er = expr(expr.PI * 2)
+    assert isinstance(er, ExprRef)
+    assert repr(er) == "ExprRef({\n  expr: (PI * 2)\n})"
+
+
+def test_expression_function_string():
+    # expr() can only work with str
+    er = expr("2 * 2")
+    assert isinstance(er, ExprRef)
+    assert repr(er) == "ExprRef({\n  expr: '2 * 2'\n})"
+
+
+def test_expression_function_nostring():
+    # expr() can only work with str otherwise
+    # should raise a SchemaValidationError
+    with pytest.raises(ValidationError):
+        expr(2 * 2)
+
+    with pytest.raises(ValidationError):
+        expr(["foo", "bah"])
diff --git a/tests/utils/tests/test_core.py b/tests/utils/tests/test_core.py
index e0b57567..91dd93ea 100644
--- a/tests/utils/tests/test_core.py
+++ b/tests/utils/tests/test_core.py
@@ -14,13 +14,13 @@ FAKE_CHANNELS_MODULE = '''
 from altair.utils import schemapi
 
 
-class FieldChannel(object):
+class FieldChannel:
     def __init__(self, shorthand, **kwargs):
         kwargs['shorthand'] = shorthand
         return super(FieldChannel, self).__init__(**kwargs)
 
 
-class ValueChannel(object):
+class ValueChannel:
     def __init__(self, value, **kwargs):
         kwargs['value'] = value
         return super(ValueChannel, self).__init__(**kwargs)
@@ -79,7 +79,7 @@ def test_parse_shorthand():
 
     # Fields alone
     check("foobar", field="foobar")
-    check("blah:(fd ", field="blah:(fd ")
+    check(r"blah\:(fd ", field=r"blah\:(fd ")
 
     # Fields with type
     check("foobar:quantitative", type="quantitative", field="foobar")
@@ -101,14 +101,14 @@ def test_parse_shorthand():
 
     # check that invalid arguments are not split-out
     check("invalid(blah)", field="invalid(blah)")
-    check("blah:invalid", field="blah:invalid")
-    check("invalid(blah):invalid", field="invalid(blah):invalid")
+    check(r"blah\:invalid", field=r"blah\:invalid")
+    check(r"invalid(blah)\:invalid", field=r"invalid(blah)\:invalid")
 
     # check parsing in presence of strange characters
     check(
-        "average(a b:(c\nd):Q",
+        r"average(a b\:(c\nd):Q",
         aggregate="average",
-        field="a b:(c\nd",
+        field=r"a b\:(c\nd",
         type="quantitative",
     )
 
@@ -281,3 +281,10 @@ def test_infer_encoding_types_with_condition():
         ),
     )
     assert infer_encoding_types(args, kwds, channels) == expected
+
+
+def test_invalid_data_type():
+    with pytest.raises(
+        ValueError, match=r'"\(fd " is not one of the valid encoding data types'
+    ):
+        parse_shorthand(r"blah:(fd ")
diff --git a/tests/utils/tests/test_schemapi.py b/tests/utils/tests/test_schemapi.py
index 9895d57a..9edc7695 100644
--- a/tests/utils/tests/test_schemapi.py
+++ b/tests/utils/tests/test_schemapi.py
@@ -2,6 +2,7 @@
 # tools/generate_schema_wrapper.py. Do not modify directly.
 import copy
 import io
+import inspect
 import json
 import jsonschema
 import re
@@ -377,9 +378,6 @@ def test_schema_validation_error():
     assert isinstance(the_err, SchemaValidationError)
     message = str(the_err)
 
-    assert message.startswith("Invalid specification")
-    assert "test_schemapi.MySchema->a" in message
-    assert "validating {!r}".format(the_err.validator) in message
     assert the_err.message in message
 
 
@@ -467,36 +465,77 @@ def chart_example_invalid_y_option_value_with_condition():
     [
         (
             chart_example_invalid_y_option,
-            r"schema.channels.X.*"
-            + r"Additional properties are not allowed \('unknown' was unexpected\)",
+            inspect.cleandoc(
+                r"""`X` has no parameter named 'unknown'
+
+                Existing parameter names are:
+                shorthand      bin      scale   timeUnit   
+                aggregate      field    sort    title      
+                axis           impute   stack   type       
+                bandPosition                               
+
+                See the help for `X` to read the full description of these parameters"""  # noqa: W291
+            ),
         ),
         (
-            chart_example_invalid_y_option_value,
-            r"schema.channels.Y.*"
-            + r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
-            + r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
+            chart_example_layer,
+            inspect.cleandoc(
+                r"""`VConcatChart` has no parameter named 'width'
+
+                Existing parameter names are:
+                vconcat      center     description   params    title       
+                autosize     config     name          resolve   transform   
+                background   data       padding       spacing   usermeta    
+                bounds       datasets                                       
+
+                See the help for `VConcatChart` to read the full description of these parameters"""  # noqa: W291
+            ),
         ),
         (
-            chart_example_layer,
-            r"api.VConcatChart.*"
-            + r"Additional properties are not allowed \('width' was unexpected\)",
+            chart_example_invalid_y_option_value,
+            inspect.cleandoc(
+                r"""'asdf' is an invalid value for `stack`:
+
+                'asdf' is not one of \['zero', 'center', 'normalize'\]
+                'asdf' is not of type 'null'
+                'asdf' is not of type 'boolean'"""
+            ),
         ),
         (
             chart_example_invalid_y_option_value_with_condition,
-            r"schema.channels.Y.*"
-            + r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
-            + r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
+            inspect.cleandoc(
+                r"""'asdf' is an invalid value for `stack`:
+
+                'asdf' is not one of \['zero', 'center', 'normalize'\]
+                'asdf' is not of type 'null'
+                'asdf' is not of type 'boolean'"""
+            ),
         ),
         (
             chart_example_hconcat,
-            r"schema.core.TitleParams.*"
-            + r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'string'.*"
-            + r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'array'",
+            inspect.cleandoc(
+                r"""'{'text': 'Horsepower', 'align': 'right'}' is an invalid value for `title`:
+
+                {'text': 'Horsepower', 'align': 'right'} is not of type 'string'
+                {'text': 'Horsepower', 'align': 'right'} is not of type 'array'"""
+            ),
         ),
         (
             chart_example_invalid_channel_and_condition,
-            r"schema.core.Encoding->encoding.*"
-            + r"Additional properties are not allowed \('invalidChannel' was unexpected\)",
+            inspect.cleandoc(
+                r"""`Encoding` has no parameter named 'invalidChannel'
+
+                Existing parameter names are:
+                angle         key          order     strokeDash      tooltip   xOffset   
+                color         latitude     radius    strokeOpacity   url       y         
+                description   latitude2    radius2   strokeWidth     x         y2        
+                detail        longitude    shape     text            x2        yError    
+                fill          longitude2   size      theta           xError    yError2   
+                fillOpacity   opacity      stroke    theta2          xError2   yOffset   
+                href                                                                     
+
+                See the help for `Encoding` to read the full description of these parameters"""  # noqa: W291
+            ),
         ),
     ],
 )
diff --git a/tests/vegalite/v5/tests/test_geo_interface.py b/tests/vegalite/v5/tests/test_geo_interface.py
index 15a55bb4..613cf006 100644
--- a/tests/vegalite/v5/tests/test_geo_interface.py
+++ b/tests/vegalite/v5/tests/test_geo_interface.py
@@ -3,7 +3,7 @@ import altair.vegalite.v5 as alt
 
 
 def geom_obj(geom):
-    class Geom(object):
+    class Geom:
         pass
 
     geom_obj = Geom()
diff --git a/tools/schemapi/tests/test_schemapi.py b/tools/schemapi/tests/test_schemapi.py
index f8ad19c9..bb3bbae7 100644
--- a/tools/schemapi/tests/test_schemapi.py
+++ b/tools/schemapi/tests/test_schemapi.py
@@ -1,5 +1,6 @@
 import copy
 import io
+import inspect
 import json
 import jsonschema
 import re
@@ -375,9 +376,6 @@ def test_schema_validation_error():
     assert isinstance(the_err, SchemaValidationError)
     message = str(the_err)
 
-    assert message.startswith("Invalid specification")
-    assert "test_schemapi.MySchema->a" in message
-    assert "validating {!r}".format(the_err.validator) in message
     assert the_err.message in message
 
 
@@ -465,36 +463,77 @@ def chart_example_invalid_y_option_value_with_condition():
     [
         (
             chart_example_invalid_y_option,
-            r"schema.channels.X.*"
-            + r"Additional properties are not allowed \('unknown' was unexpected\)",
+            inspect.cleandoc(
+                r"""`X` has no parameter named 'unknown'
+
+                Existing parameter names are:
+                shorthand      bin      scale   timeUnit   
+                aggregate      field    sort    title      
+                axis           impute   stack   type       
+                bandPosition                               
+
+                See the help for `X` to read the full description of these parameters"""  # noqa: W291
+            ),
         ),
         (
-            chart_example_invalid_y_option_value,
-            r"schema.channels.Y.*"
-            + r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
-            + r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
+            chart_example_layer,
+            inspect.cleandoc(
+                r"""`VConcatChart` has no parameter named 'width'
+
+                Existing parameter names are:
+                vconcat      center     description   params    title       
+                autosize     config     name          resolve   transform   
+                background   data       padding       spacing   usermeta    
+                bounds       datasets                                       
+
+                See the help for `VConcatChart` to read the full description of these parameters"""  # noqa: W291
+            ),
         ),
         (
-            chart_example_layer,
-            r"api.VConcatChart.*"
-            + r"Additional properties are not allowed \('width' was unexpected\)",
+            chart_example_invalid_y_option_value,
+            inspect.cleandoc(
+                r"""'asdf' is an invalid value for `stack`:
+
+                'asdf' is not one of \['zero', 'center', 'normalize'\]
+                'asdf' is not of type 'null'
+                'asdf' is not of type 'boolean'"""
+            ),
         ),
         (
             chart_example_invalid_y_option_value_with_condition,
-            r"schema.channels.Y.*"
-            + r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
-            + r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
+            inspect.cleandoc(
+                r"""'asdf' is an invalid value for `stack`:
+
+                'asdf' is not one of \['zero', 'center', 'normalize'\]
+                'asdf' is not of type 'null'
+                'asdf' is not of type 'boolean'"""
+            ),
         ),
         (
             chart_example_hconcat,
-            r"schema.core.TitleParams.*"
-            + r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'string'.*"
-            + r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'array'",
+            inspect.cleandoc(
+                r"""'{'text': 'Horsepower', 'align': 'right'}' is an invalid value for `title`:
+
+                {'text': 'Horsepower', 'align': 'right'} is not of type 'string'
+                {'text': 'Horsepower', 'align': 'right'} is not of type 'array'"""
+            ),
         ),
         (
             chart_example_invalid_channel_and_condition,
-            r"schema.core.Encoding->encoding.*"
-            + r"Additional properties are not allowed \('invalidChannel' was unexpected\)",
+            inspect.cleandoc(
+                r"""`Encoding` has no parameter named 'invalidChannel'
+
+                Existing parameter names are:
+                angle         key          order     strokeDash      tooltip   xOffset   
+                color         latitude     radius    strokeOpacity   url       y         
+                description   latitude2    radius2   strokeWidth     x         y2        
+                detail        longitude    shape     text            x2        yError    
+                fill          longitude2   size      theta           xError    yError2   
+                fillOpacity   opacity      stroke    theta2          xError2   yOffset   
+                href                                                                     
+
+                See the help for `Encoding` to read the full description of these parameters"""  # noqa: W291
+            ),
         ),
     ],
 )