Repository: altair-viz/altair
Pull request #2855 (2023-01-26 14:07:24)

Update Large Datasets documentation with VegaFusion 1.0 information
First off, thanks to @binste for including VegaFusion in the new Large Datasets documentation page added in https://github.com/altair-viz/altair/pull/2755.

This PR updates that section with some additional information now that  [VegaFusion 1.0](https://vegafusion.io/posts/2023/2023-01-21_Release_1.0.0.html) is out.

In particular, I want to encourage people to try the VegaFusion mime renderer first, as this provides benefits for the vast majority of Altair charts without requiring a Jupyter Widget extension.

Thanks!



jonmmease: Thanks for the review and helpful suggestions @binste!  See what you think of the latest revision

Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index ff590a06..3f3bad10 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,7 +14,7 @@ jobs:
         python-version: "3.10"
     - name: Check formatting with black
       run: |
-        pip install black
+        pip install "black<24"
         black --diff --color .
         black --check .
     - name: Lint with flake8
diff --git a/RELEASING.md b/RELEASING.md
index c4b7626a..bf051120 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -38,7 +38,7 @@
 8. Tag the release:
 
        git tag -a v5.0.0 -m "version 5.0.0 release"
-       git push upstream v2.0.0
+       git push upstream v5.0.0
 
 9. Build source & wheel distributions:
 
@@ -48,7 +48,7 @@
 
 10. publish to PyPI (Requires correct PyPI owner permissions):
 
-       twine upload dist/*
+        twine upload dist/*
 
 11. build and publish docs (Requires write-access to altair-viz/altair-viz.github.io):
 
diff --git a/altair/__init__.py b/altair/__init__.py
index 86b9498e..caa9ccbe 100644
--- a/altair/__init__.py
+++ b/altair/__init__.py
@@ -1,6 +1,590 @@
 # flake8: noqa
 __version__ = "4.3.0.dev0"
 
+
+# The content of __all__ is automatically written by
+# tools/update_init_file.py. Do not modify directly.
+__all__ = [
+    "Aggregate",
+    "AggregateOp",
+    "AggregateTransform",
+    "AggregatedFieldDef",
+    "Align",
+    "AllSortString",
+    "Angle",
+    "AngleDatum",
+    "AngleValue",
+    "AnyMark",
+    "AnyMarkConfig",
+    "AreaConfig",
+    "ArgmaxDef",
+    "ArgminDef",
+    "AutoSizeParams",
+    "AutosizeType",
+    "Axis",
+    "AxisConfig",
+    "AxisOrient",
+    "AxisResolveMap",
+    "BarConfig",
+    "BaseTitleNoValueRefs",
+    "Baseline",
+    "Bin",
+    "BinExtent",
+    "BinParams",
+    "BinTransform",
+    "BindCheckbox",
+    "BindDirect",
+    "BindInput",
+    "BindRadioSelect",
+    "BindRange",
+    "Binding",
+    "Blend",
+    "BoxPlot",
+    "BoxPlotConfig",
+    "BoxPlotDef",
+    "BrushConfig",
+    "CalculateTransform",
+    "Categorical",
+    "Chart",
+    "Color",
+    "ColorDatum",
+    "ColorDef",
+    "ColorName",
+    "ColorScheme",
+    "ColorValue",
+    "Column",
+    "CompositeMark",
+    "CompositeMarkDef",
+    "CompositionConfig",
+    "ConcatChart",
+    "ConcatSpecGenericSpec",
+    "ConditionalAxisColor",
+    "ConditionalAxisLabelAlign",
+    "ConditionalAxisLabelBaseline",
+    "ConditionalAxisLabelFontStyle",
+    "ConditionalAxisLabelFontWeight",
+    "ConditionalAxisNumber",
+    "ConditionalAxisNumberArray",
+    "ConditionalAxisPropertyAlignnull",
+    "ConditionalAxisPropertyColornull",
+    "ConditionalAxisPropertyFontStylenull",
+    "ConditionalAxisPropertyFontWeightnull",
+    "ConditionalAxisPropertyTextBaselinenull",
+    "ConditionalAxisPropertynumberArraynull",
+    "ConditionalAxisPropertynumbernull",
+    "ConditionalAxisPropertystringnull",
+    "ConditionalAxisString",
+    "ConditionalMarkPropFieldOrDatumDef",
+    "ConditionalMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalParameterMarkPropFieldOrDatumDef",
+    "ConditionalParameterMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalParameterStringFieldDef",
+    "ConditionalParameterValueDefGradientstringnullExprRef",
+    "ConditionalParameterValueDefTextExprRef",
+    "ConditionalParameterValueDefnumber",
+    "ConditionalParameterValueDefnumberArrayExprRef",
+    "ConditionalParameterValueDefnumberExprRef",
+    "ConditionalParameterValueDefstringExprRef",
+    "ConditionalParameterValueDefstringnullExprRef",
+    "ConditionalPredicateMarkPropFieldOrDatumDef",
+    "ConditionalPredicateMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalPredicateStringFieldDef",
+    "ConditionalPredicateValueDefAlignnullExprRef",
+    "ConditionalPredicateValueDefColornullExprRef",
+    "ConditionalPredicateValueDefFontStylenullExprRef",
+    "ConditionalPredicateValueDefFontWeightnullExprRef",
+    "ConditionalPredicateValueDefGradientstringnullExprRef",
+    "ConditionalPredicateValueDefTextBaselinenullExprRef",
+    "ConditionalPredicateValueDefTextExprRef",
+    "ConditionalPredicateValueDefnumber",
+    "ConditionalPredicateValueDefnumberArrayExprRef",
+    "ConditionalPredicateValueDefnumberArraynullExprRef",
+    "ConditionalPredicateValueDefnumberExprRef",
+    "ConditionalPredicateValueDefnumbernullExprRef",
+    "ConditionalPredicateValueDefstringExprRef",
+    "ConditionalPredicateValueDefstringnullExprRef",
+    "ConditionalStringFieldDef",
+    "ConditionalValueDefGradientstringnullExprRef",
+    "ConditionalValueDefTextExprRef",
+    "ConditionalValueDefnumber",
+    "ConditionalValueDefnumberArrayExprRef",
+    "ConditionalValueDefnumberExprRef",
+    "ConditionalValueDefstringExprRef",
+    "ConditionalValueDefstringnullExprRef",
+    "Config",
+    "CsvDataFormat",
+    "Cursor",
+    "Cyclical",
+    "Data",
+    "DataFormat",
+    "DataSource",
+    "Datasets",
+    "DateTime",
+    "DatumChannelMixin",
+    "DatumDef",
+    "Day",
+    "DensityTransform",
+    "DerivedStream",
+    "Description",
+    "DescriptionValue",
+    "Detail",
+    "Dict",
+    "DictInlineDataset",
+    "DictSelectionInit",
+    "DictSelectionInitInterval",
+    "Diverging",
+    "DomainUnionWith",
+    "DsvDataFormat",
+    "Element",
+    "Encoding",
+    "EncodingSortField",
+    "ErrorBand",
+    "ErrorBandConfig",
+    "ErrorBandDef",
+    "ErrorBar",
+    "ErrorBarConfig",
+    "ErrorBarDef",
+    "ErrorBarExtent",
+    "EventStream",
+    "EventType",
+    "Expr",
+    "ExprRef",
+    "Facet",
+    "FacetChart",
+    "FacetEncodingFieldDef",
+    "FacetFieldDef",
+    "FacetMapping",
+    "FacetSpec",
+    "FacetedEncoding",
+    "FacetedUnitSpec",
+    "Field",
+    "FieldChannelMixin",
+    "FieldDefWithoutScale",
+    "FieldEqualPredicate",
+    "FieldGTEPredicate",
+    "FieldGTPredicate",
+    "FieldLTEPredicate",
+    "FieldLTPredicate",
+    "FieldName",
+    "FieldOneOfPredicate",
+    "FieldOrDatumDefWithConditionDatumDefGradientstringnull",
+    "FieldOrDatumDefWithConditionDatumDefnumber",
+    "FieldOrDatumDefWithConditionDatumDefnumberArray",
+    "FieldOrDatumDefWithConditionDatumDefstringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefGradientstringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefTypeForShapestringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefnumber",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefnumberArray",
+    "FieldOrDatumDefWithConditionStringDatumDefText",
+    "FieldOrDatumDefWithConditionStringFieldDefText",
+    "FieldOrDatumDefWithConditionStringFieldDefstring",
+    "FieldRange",
+    "FieldRangePredicate",
+    "FieldValidPredicate",
+    "Fill",
+    "FillDatum",
+    "FillOpacity",
+    "FillOpacityDatum",
+    "FillOpacityValue",
+    "FillValue",
+    "FilterTransform",
+    "Fit",
+    "FlattenTransform",
+    "FoldTransform",
+    "FontStyle",
+    "FontWeight",
+    "Generator",
+    "GenericUnitSpecEncodingAnyMark",
+    "GeoJsonFeature",
+    "GeoJsonFeatureCollection",
+    "Gradient",
+    "GradientStop",
+    "GraticuleGenerator",
+    "GraticuleParams",
+    "HConcatChart",
+    "HConcatSpecGenericSpec",
+    "Header",
+    "HeaderConfig",
+    "HexColor",
+    "Href",
+    "HrefValue",
+    "Impute",
+    "ImputeMethod",
+    "ImputeParams",
+    "ImputeSequence",
+    "ImputeTransform",
+    "InlineData",
+    "InlineDataset",
+    "Interpolate",
+    "IntervalSelectionConfig",
+    "IntervalSelectionConfigWithoutType",
+    "JoinAggregateFieldDef",
+    "JoinAggregateTransform",
+    "JsonDataFormat",
+    "Key",
+    "LabelOverlap",
+    "LatLongDef",
+    "LatLongFieldDef",
+    "Latitude",
+    "Latitude2",
+    "Latitude2Datum",
+    "Latitude2Value",
+    "LatitudeDatum",
+    "LayerChart",
+    "LayerRepeatMapping",
+    "LayerRepeatSpec",
+    "LayerSpec",
+    "LayoutAlign",
+    "Legend",
+    "LegendBinding",
+    "LegendConfig",
+    "LegendOrient",
+    "LegendResolveMap",
+    "LegendStreamBinding",
+    "LineConfig",
+    "LinearGradient",
+    "LocalMultiTimeUnit",
+    "LocalSingleTimeUnit",
+    "Locale",
+    "LoessTransform",
+    "LogicalAndPredicate",
+    "LogicalNotPredicate",
+    "LogicalOrPredicate",
+    "Longitude",
+    "Longitude2",
+    "Longitude2Datum",
+    "Longitude2Value",
+    "LongitudeDatum",
+    "LookupData",
+    "LookupSelection",
+    "LookupTransform",
+    "Mark",
+    "MarkConfig",
+    "MarkDef",
+    "MarkPropDefGradientstringnull",
+    "MarkPropDefnumber",
+    "MarkPropDefnumberArray",
+    "MarkPropDefstringnullTypeForShape",
+    "MarkType",
+    "MaxRowsError",
+    "MergedStream",
+    "Month",
+    "MultiTimeUnit",
+    "NamedData",
+    "NonArgAggregateOp",
+    "NonLayerRepeatSpec",
+    "NonNormalizedSpec",
+    "NumberLocale",
+    "NumericArrayMarkPropDef",
+    "NumericMarkPropDef",
+    "OffsetDef",
+    "Opacity",
+    "OpacityDatum",
+    "OpacityValue",
+    "Order",
+    "OrderFieldDef",
+    "OrderValue",
+    "OrderValueDef",
+    "Orient",
+    "Orientation",
+    "OverlayMarkDef",
+    "Padding",
+    "Parameter",
+    "ParameterExpression",
+    "ParameterExtent",
+    "ParameterName",
+    "ParameterPredicate",
+    "Parse",
+    "ParseValue",
+    "PivotTransform",
+    "PointSelectionConfig",
+    "PointSelectionConfigWithoutType",
+    "PolarDef",
+    "Position2Def",
+    "PositionDatumDef",
+    "PositionDatumDefBase",
+    "PositionDef",
+    "PositionFieldDef",
+    "PositionFieldDefBase",
+    "PositionValueDef",
+    "Predicate",
+    "PredicateComposition",
+    "PrimitiveValue",
+    "Projection",
+    "ProjectionConfig",
+    "ProjectionType",
+    "QuantileTransform",
+    "RadialGradient",
+    "Radius",
+    "Radius2",
+    "Radius2Datum",
+    "Radius2Value",
+    "RadiusDatum",
+    "RadiusValue",
+    "RangeConfig",
+    "RangeEnum",
+    "RangeRaw",
+    "RangeRawArray",
+    "RangeScheme",
+    "RectConfig",
+    "RegressionTransform",
+    "RelativeBandSize",
+    "RepeatChart",
+    "RepeatMapping",
+    "RepeatRef",
+    "RepeatSpec",
+    "Resolve",
+    "ResolveMode",
+    "Root",
+    "Row",
+    "RowColLayoutAlign",
+    "RowColboolean",
+    "RowColnumber",
+    "RowColumnEncodingFieldDef",
+    "SCHEMA_URL",
+    "SCHEMA_VERSION",
+    "SampleTransform",
+    "Scale",
+    "ScaleBinParams",
+    "ScaleBins",
+    "ScaleConfig",
+    "ScaleDatumDef",
+    "ScaleFieldDef",
+    "ScaleInterpolateEnum",
+    "ScaleInterpolateParams",
+    "ScaleResolveMap",
+    "ScaleType",
+    "SchemaBase",
+    "SchemeParams",
+    "SecondaryFieldDef",
+    "SelectionConfig",
+    "SelectionExpression",
+    "SelectionInit",
+    "SelectionInitInterval",
+    "SelectionInitIntervalMapping",
+    "SelectionInitMapping",
+    "SelectionParameter",
+    "SelectionPredicateComposition",
+    "SelectionResolution",
+    "SelectionType",
+    "SequenceGenerator",
+    "SequenceParams",
+    "SequentialMultiHue",
+    "SequentialSingleHue",
+    "Shape",
+    "ShapeDatum",
+    "ShapeDef",
+    "ShapeValue",
+    "SharedEncoding",
+    "SingleDefUnitChannel",
+    "SingleTimeUnit",
+    "Size",
+    "SizeDatum",
+    "SizeValue",
+    "Sort",
+    "SortArray",
+    "SortByChannel",
+    "SortByChannelDesc",
+    "SortByEncoding",
+    "SortField",
+    "SortOrder",
+    "Spec",
+    "SphereGenerator",
+    "StackOffset",
+    "StackTransform",
+    "StandardType",
+    "Step",
+    "StepFor",
+    "Stream",
+    "StringFieldDef",
+    "StringFieldDefWithCondition",
+    "StringValueDefWithCondition",
+    "Stroke",
+    "StrokeCap",
+    "StrokeDash",
+    "StrokeDashDatum",
+    "StrokeDashValue",
+    "StrokeDatum",
+    "StrokeJoin",
+    "StrokeOpacity",
+    "StrokeOpacityDatum",
+    "StrokeOpacityValue",
+    "StrokeValue",
+    "StrokeWidth",
+    "StrokeWidthDatum",
+    "StrokeWidthValue",
+    "StyleConfigIndex",
+    "SymbolShape",
+    "TOPLEVEL_ONLY_KEYS",
+    "Text",
+    "TextBaseline",
+    "TextDatum",
+    "TextDef",
+    "TextDirection",
+    "TextValue",
+    "Theta",
+    "Theta2",
+    "Theta2Datum",
+    "Theta2Value",
+    "ThetaDatum",
+    "ThetaValue",
+    "TickConfig",
+    "TickCount",
+    "TimeInterval",
+    "TimeIntervalStep",
+    "TimeLocale",
+    "TimeUnit",
+    "TimeUnitParams",
+    "TimeUnitTransform",
+    "Title",
+    "TitleAnchor",
+    "TitleConfig",
+    "TitleFrame",
+    "TitleOrient",
+    "TitleParams",
+    "Tooltip",
+    "TooltipContent",
+    "TooltipValue",
+    "TopLevelConcatSpec",
+    "TopLevelFacetSpec",
+    "TopLevelHConcatSpec",
+    "TopLevelLayerSpec",
+    "TopLevelMixin",
+    "TopLevelRepeatSpec",
+    "TopLevelSelectionParameter",
+    "TopLevelSpec",
+    "TopLevelUnitSpec",
+    "TopLevelVConcatSpec",
+    "TopoDataFormat",
+    "Transform",
+    "Type",
+    "TypeForShape",
+    "TypedFieldDef",
+    "URI",
+    "Undefined",
+    "UnitSpec",
+    "UnitSpecWithFrame",
+    "Url",
+    "UrlData",
+    "UrlValue",
+    "UtcMultiTimeUnit",
+    "UtcSingleTimeUnit",
+    "VConcatChart",
+    "VConcatSpecGenericSpec",
+    "VEGAEMBED_VERSION",
+    "VEGALITE_VERSION",
+    "VEGA_VERSION",
+    "ValueChannelMixin",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefGradientstringnull",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefTypeForShapestringnull",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefnumber",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefnumberArray",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefstringnull",
+    "ValueDefWithConditionStringFieldDefText",
+    "ValueDefnumber",
+    "ValueDefnumberwidthheightExprRef",
+    "VariableParameter",
+    "Vector10string",
+    "Vector12string",
+    "Vector2DateTime",
+    "Vector2Vector2number",
+    "Vector2boolean",
+    "Vector2number",
+    "Vector2string",
+    "Vector3number",
+    "Vector7string",
+    "VegaLite",
+    "VegaLiteSchema",
+    "ViewBackground",
+    "ViewConfig",
+    "WindowEventType",
+    "WindowFieldDef",
+    "WindowOnlyOp",
+    "WindowTransform",
+    "X",
+    "X2",
+    "X2Datum",
+    "X2Value",
+    "XDatum",
+    "XError",
+    "XError2",
+    "XError2Value",
+    "XErrorValue",
+    "XOffset",
+    "XOffsetDatum",
+    "XOffsetValue",
+    "XValue",
+    "Y",
+    "Y2",
+    "Y2Datum",
+    "Y2Value",
+    "YDatum",
+    "YError",
+    "YError2",
+    "YError2Value",
+    "YErrorValue",
+    "YOffset",
+    "YOffsetDatum",
+    "YOffsetValue",
+    "YValue",
+    "api",
+    "binding",
+    "binding_checkbox",
+    "binding_radio",
+    "binding_range",
+    "binding_select",
+    "channels",
+    "check_fields_and_encodings",
+    "concat",
+    "condition",
+    "core",
+    "curry",
+    "data",
+    "data_transformers",
+    "datasets",
+    "datum",
+    "default_data_transformer",
+    "display",
+    "expr",
+    "graticule",
+    "hconcat",
+    "layer",
+    "limit_rows",
+    "load_ipython_extension",
+    "load_schema",
+    "mixins",
+    "overload",
+    "param",
+    "parse_shorthand",
+    "pipe",
+    "renderers",
+    "repeat",
+    "sample",
+    "schema",
+    "selection",
+    "selection_interval",
+    "selection_point",
+    "sequence",
+    "sphere",
+    "theme",
+    "themes",
+    "to_csv",
+    "to_json",
+    "to_values",
+    "topo_feature",
+    "utils",
+    "v5",
+    "value",
+    "vconcat",
+    "vegalite",
+    "with_property_setters",
+]
+
+
+def __dir__():
+    return __all__
+
+
 from .vegalite import *
 
 
diff --git a/altair/utils/deprecation.py b/altair/utils/deprecation.py
index 9deca223..0230fda9 100644
--- a/altair/utils/deprecation.py
+++ b/altair/utils/deprecation.py
@@ -65,6 +65,7 @@ def _deprecate(obj, name=None, message=None):
             warnings.warn(message, AltairDeprecationWarning)
             return obj(*args, **kwargs)
 
+        new_obj._deprecated = True
         return new_obj
     else:
         raise ValueError("Cannot deprecate object of type {}".format(type(obj)))
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index fab24a4a..0c8f9b27 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -5,7 +5,7 @@ import contextlib
 import inspect
 import json
 import textwrap
-from typing import Any
+from typing import Any, Sequence, List
 
 import jsonschema
 import jsonschema.exceptions
@@ -66,9 +66,50 @@ def validate_jsonschema(spec, schema, rootschema=None):
     if hasattr(validator_cls, "FORMAT_CHECKER"):
         validator_kwargs["format_checker"] = validator_cls.FORMAT_CHECKER
     validator = validator_cls(schema, **validator_kwargs)
-    error = jsonschema.exceptions.best_match(validator.iter_errors(spec))
-    if error is not None:
-        raise error
+    errors = list(validator.iter_errors(spec))
+    if errors:
+        errors = _get_most_relevant_errors(errors)
+        main_error = errors[0]
+        # They can be used to craft more helpful error messages when this error
+        # is being converted to a SchemaValidationError
+        main_error._additional_errors = errors[1:]
+        raise main_error
+
+
+def _get_most_relevant_errors(
+    errors: Sequence[jsonschema.exceptions.ValidationError],
+) -> List[jsonschema.exceptions.ValidationError]:
+    if len(errors) == 0:
+        return []
+
+    # Go to lowest level in schema where an error happened as these give
+    # the most relevant error messages
+    lowest_level = errors[0]
+    while lowest_level.context:
+        lowest_level = lowest_level.context[0]
+
+    most_relevant_errors = []
+    if lowest_level.validator == "additionalProperties":
+        # For these errors, the message is already informative enough and the other
+        # errors on the lowest level are not helpful for a user but instead contain
+        # the same information in a more verbose way
+        most_relevant_errors = [lowest_level]
+    else:
+        parent = lowest_level.parent
+        if parent is None:
+            # In this case we are still at the top level and can return all errors
+            most_relevant_errors = errors
+        else:
+            # Return all errors of the lowest level out of which
+            # we can construct more informative error messages
+            most_relevant_errors = lowest_level.parent.context
+
+    # This should never happen but might still be good to test for it as else
+    # the original error would just slip through without being raised
+    if len(most_relevant_errors) == 0:
+        raise Exception("Could not determine the most relevant errors") from errors[0]
+
+    return most_relevant_errors
 
 
 def _subclasses(cls):
@@ -82,18 +123,14 @@ def _subclasses(cls):
             yield cls
 
 
-def _todict(obj, validate, context):
+def _todict(obj, context):
     """Convert an object to a dict representation."""
     if isinstance(obj, SchemaBase):
-        return obj.to_dict(validate=validate, context=context)
+        return obj.to_dict(validate=False, context=context)
     elif isinstance(obj, (list, tuple, np.ndarray)):
-        return [_todict(v, validate, context) for v in obj]
+        return [_todict(v, context) for v in obj]
     elif isinstance(obj, dict):
-        return {
-            k: _todict(v, validate, context)
-            for k, v in obj.items()
-            if v is not Undefined
-        }
+        return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
     elif hasattr(obj, "to_dict"):
         return obj.to_dict()
     elif isinstance(obj, np.number):
@@ -117,24 +154,9 @@ class SchemaValidationError(jsonschema.ValidationError):
     """A wrapper for jsonschema.ValidationError with friendlier traceback"""
 
     def __init__(self, obj, err):
-        super(SchemaValidationError, self).__init__(**self._get_contents(err))
+        super(SchemaValidationError, self).__init__(**err._contents())
         self.obj = obj
-
-    @staticmethod
-    def _get_contents(err):
-        """Get a dictionary with the contents of a ValidationError"""
-        try:
-            # works in jsonschema 2.3 or later
-            contents = err._contents()
-        except AttributeError:
-            try:
-                # works in Python >=3.4
-                spec = inspect.getfullargspec(err.__init__)
-            except AttributeError:
-                # works in Python <3.4
-                spec = inspect.getargspec(err.__init__)
-            contents = {key: getattr(err, key) for key in spec.args[1:]}
-        return contents
+        self._additional_errors = getattr(err, "_additional_errors", [])
 
     def __str__(self):
         cls = self.obj.__class__
@@ -145,13 +167,18 @@ class SchemaValidationError(jsonschema.ValidationError):
             for val in schema_path[:-1]
             if val not in ("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}
 
         {}
         """.format(
-            schema_path, self.validator, self.message
+            schema_path, self.validator, message
         )
 
 
@@ -327,11 +354,9 @@ class SchemaBase(object):
 
         Parameters
         ----------
-        validate : boolean or string
+        validate : boolean
             If True (default), then validate the output dictionary
-            against the schema. If "deep" then recursively validate
-            all objects in the spec. This takes much more time, but
-            it results in friendlier tracebacks for large objects.
+            against the schema.
         ignore : list
             A list of keys to ignore. This will *not* passed to child to_dict
             function calls.
@@ -353,10 +378,9 @@ class SchemaBase(object):
             context = {}
         if ignore is None:
             ignore = []
-        sub_validate = "deep" if validate == "deep" else False
 
         if self._args and not self._kwds:
-            result = _todict(self._args[0], validate=sub_validate, context=context)
+            result = _todict(self._args[0], context=context)
         elif not self._args:
             kwds = self._kwds.copy()
             # parsed_shorthand is added by FieldChannelMixin.
@@ -384,7 +408,6 @@ class SchemaBase(object):
             }
             result = _todict(
                 kwds,
-                validate=sub_validate,
                 context=context,
             )
         else:
@@ -406,11 +429,9 @@ class SchemaBase(object):
 
         Parameters
         ----------
-        validate : boolean or string
+        validate : boolean
             If True (default), then validate the output dictionary
-            against the schema. If "deep" then recursively validate
-            all objects in the spec. This takes much more time, but
-            it results in friendlier tracebacks for large objects.
+            against the schema.
         ignore : list
             A list of keys to ignore. This will *not* passed to child to_dict
             function calls.
@@ -516,7 +537,7 @@ class SchemaBase(object):
         Validate a property against property schema in the context of the
         rootschema
         """
-        value = _todict(value, validate=False, context={})
+        value = _todict(value, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
         return validate_jsonschema(
             value, props.get(name, {}), rootschema=cls._rootschema or cls._schema
diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py
index e5bd2cc8..4ca266f4 100644
--- a/altair/vegalite/v5/api.py
+++ b/altair/vegalite/v5/api.py
@@ -556,17 +556,7 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         context["top_level"] = False
         kwargs["context"] = context
 
-        try:
-            dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
-        except jsonschema.ValidationError:
-            dct = None
-
-        # If we hit an error, then re-convert with validate='deep' to get
-        # a more useful traceback. We don't do this by default because it's
-        # much slower in the case that there are no errors.
-        if dct is None:
-            kwargs["validate"] = "deep"
-            dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
+        dct = super(TopLevelMixin, copy).to_dict(*args, **kwargs)
 
         # TODO: following entries are added after validation. Should they be validated?
         if is_top_level:
@@ -2282,24 +2272,38 @@ def _check_if_can_be_layered(spec):
     if encoding is not Undefined:
         for channel in ["row", "column", "facet"]:
             if _get(encoding, channel) is not Undefined:
-                raise ValueError("Faceted charts cannot be layered.")
+                raise ValueError(
+                    "Faceted charts cannot be layered. Instead, layer the charts before faceting."
+                )
     if isinstance(spec, (Chart, LayerChart)):
         return
 
     if not isinstance(spec, (core.SchemaBase, dict)):
         raise ValueError("Only chart objects can be layered.")
     if _get(spec, "facet") is not Undefined:
-        raise ValueError("Faceted charts cannot be layered.")
+        raise ValueError(
+            "Faceted charts cannot be layered. Instead, layer the charts before faceting."
+        )
     if isinstance(spec, FacetChart) or _get(spec, "facet") is not Undefined:
-        raise ValueError("Faceted charts cannot be layered.")
+        raise ValueError(
+            "Faceted charts cannot be layered. Instead, layer the charts before faceting."
+        )
     if isinstance(spec, RepeatChart) or _get(spec, "repeat") is not Undefined:
-        raise ValueError("Repeat charts cannot be layered.")
+        raise ValueError(
+            "Repeat charts cannot be layered. Instead, layer the charts before repeating."
+        )
     if isinstance(spec, ConcatChart) or _get(spec, "concat") is not Undefined:
-        raise ValueError("Concatenated charts cannot be layered.")
+        raise ValueError(
+            "Concatenated charts cannot be layered. Instead, layer the charts before concatenating."
+        )
     if isinstance(spec, HConcatChart) or _get(spec, "hconcat") is not Undefined:
-        raise ValueError("Concatenated charts cannot be layered.")
+        raise ValueError(
+            "Concatenated charts cannot be layered. Instead, layer the charts before concatenating."
+        )
     if isinstance(spec, VConcatChart) or _get(spec, "vconcat") is not Undefined:
-        raise ValueError("Concatenated charts cannot be layered.")
+        raise ValueError(
+            "Concatenated charts cannot be layered. Instead, layer the charts before concatenating."
+        )
 
 
 @utils.use_signature(core.TopLevelRepeatSpec)
@@ -2869,7 +2873,6 @@ def _remove_duplicate_params(layer):
     found_params = []
 
     for subchart in subcharts:
-
         if (not hasattr(subchart, "params")) or (subchart.params is Undefined):
             continue
 
@@ -2897,7 +2900,6 @@ def _remove_duplicate_params(layer):
 
 
 def _combine_subchart_params(params, subcharts):
-
     if params is Undefined:
         params = []
 
diff --git a/doc/releases/changes.rst b/doc/releases/changes.rst
index 73be39f7..02978562 100644
--- a/doc/releases/changes.rst
+++ b/doc/releases/changes.rst
@@ -3,17 +3,30 @@
 Release Notes
 =============
 
-Version 4.3.0 (unreleased)
+Version 5.0.0 (unreleased)
 --------------------------
+
 - Update Vega-Lite from version 4.17.0 to version 5.2.0;
   see `Vega-Lite Release Notes <https://github.com/vega/vega-lite/releases>`_.
 
 Enhancements
 ~~~~~~~~~~~~
-- As described in the release notes for `Vega-Lite 5.0.0 <https://github.com/vega/vega-lite/releases/tag/v5.0.0>`_, the primary change in this release of Altair is the introduction of parameters.  There are two types of parameters, selection parameters and variable parameters.  Variable parameters are new to Altair, and while selections are not new, much of the old terminology has been deprecated.  See :ref:`gallery_slider_cutoff` for an application of variable parameters.
+
+- As described in the release notes for `Vega-Lite 5.0.0 <https://github.com/vega/vega-lite/releases/tag/v5.0.0>`_, the primary change in this release of Altair is the introduction of parameters. There are two types of parameters, selection parameters and variable parameters.  Variable parameters are new to Altair, and while selections are not new, much of the old terminology has been deprecated.  See :ref:`gallery_slider_cutoff` for an application of variable parameters (#2528).
+- Grouped bar charts and jitter are now supported using offset channels, see :ref:`gallery_grouped_bar_chart2` and :ref:`gallery_strip_plot_jitter`
+- ``vl-convert`` is now used as the default backend for saving Altair charts as svg and png files, which should simplify saving chart as it does not require external dependencies like altair_saver does (#2701).
+- The default chart width was changed from 400 to 300 (#2785).
+- Ordered pandas categorical data are now automatically encoded as sorted ordinal data (#2522)
+- The ``Title`` and ``Impute`` aliases were added for ``TitleParams`` and ``ImputeParams``, respectively (#2732).
+- 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)
 
 Grammar Changes
 ~~~~~~~~~~~~~~~
+
+- Channel options can now be set via a method based syntax in addition to the previous helper class syntax. For example, in addition to ``alt.X(..., bin=alt.Bin(...))`` it is now possible to do ``alt.X(...).bin(...)```) (#2795). See :ref:`method-based-attribute-setting` for details.
 - ``selection_single`` and ``selection_multi`` are now deprecated; use ``selection_point`` instead.  Similarly, ``type=point`` should be used instead of ``type=single`` and ``type=multi``.
 - ``add_selection`` is deprecated; use ``add_params`` instead.
 - The ``selection`` keyword argument must in many cases be replaced by ``param`` (e.g., when specifying a filter transform).
@@ -22,18 +35,50 @@ Grammar Changes
 
 Bug Fixes
 ~~~~~~~~~
+
+- Displaying a chart not longer changes the shorthand syntax of the stored spec (#2813).
+- Fixed ``disable_debug_mode`` (#2851).
+- Fixed issue where the webdriver was not working with Firefox's geckodriver (#2466).
+- Dynamically determine the jsonschema validator to avoid issues with recent jsonschema versions (#2812).
+
 Backward-Incompatible Changes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- 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.
-- Removed the Vega (v5) wrappers and deprecate rendering in Vega mode (save Chart as Vega format is still allowed).
+
+Version 4.2.2 (released Jan 27, 2023)
+-------------------------------------
+
+Bug Fixes
+~~~~~~~~~
+
+- Fix incompatibility with jsonschema < 4.5 which got introduced in Altair 4.2.1 (#2860).
+
+Version 4.2.1 (released Jan 26, 2023)
+-------------------------------------
+
+Bug Fixes
+~~~~~~~~~
+
+- Disable uri-reference format check in jsonsschema (#2771).
+- Replace ``iteritems`` with ``items`` due to pandas deprecation (#2683).
+
+Maintenance
+~~~~~~~~~~~
+
+- Add deprecation and removal warnings for Vega-Lite v3 wrappers and Vega v5 wrappers (#2843).
 
 Version 4.2.0 (released Dec 29, 2021)
 -------------------------------------
+
 - Update Vega-Lite from version 4.8.1 to version 4.17.0;
   see `Vega-Lite Release Notes <https://github.com/vega/vega-lite/releases>`_.
 
 Enhancements
 ~~~~~~~~~~~~
+
 - Pie charts are now supported through the use of ``mark_arc``. (Examples: eg.
   :ref:`gallery_pie_chart` and :ref:`gallery_radial_chart`.)
 - Support for the ``datum`` encoding specifications from Vega-Lite; see 
@@ -47,17 +92,20 @@ Enhancements
 
 Bug Fixes
 ~~~~~~~~~
+
 - Fix ``to_dict()`` for nested selections (#2120).
 - Fix item access for expressions (#2099).
 
 Version 4.1.0 (released April 1, 2020)
 --------------------------------------
+
 - Minimum Python version is now 3.6
 - Update Vega-Lite to version 4.8.1; many new features and bug fixes from Vega-Lite
   versions 4.1 through 4.8; see `Vega-Lite Release Notes <https://github.com/vega/vega-lite/releases>`_.
 
 Enhancements
 ~~~~~~~~~~~~
+
 - ``strokeDash`` encoding can now be used to control line styles (Example:
   `Multi Series Line Chart <https://altair-viz.github.io/gallery/multi_series_line.html>`_)
 - ``chart.save()`` now relies on `altair_saver <http://github.com/altair-viz/altair_saver>`_
@@ -68,6 +116,7 @@ Enhancements
 
 Bug Fixes
 ~~~~~~~~~
+
 - Support Python 3.8 (#1958)
 - Support multiple views in JupyterLab (#1986)
 - Support numpy types within specifications (#1914)
@@ -75,18 +124,22 @@ Bug Fixes
 
 Maintenance
 ~~~~~~~~~~~
+
 - Altair now uses `black <https://github.com/psf/black>`_ and
   `flake8 <https://gitlab.com/pycqa/flake8>`_ for maintaining code quality & consistency.
 
 Version 4.0.1 (released Jan 14, 2020)
 -------------------------------------
+
 Bug Fixes
 ~~~~~~~~~
+
 - Update Vega-Lite version to 4.0.2
 - Fix issue with duplicate chart divs in HTML renderer (#1888)
 
 Version 4.0.0 (released Dec 10, 2019)
 -------------------------------------
+
 Version 4.0.0 is based on Vega-Lite version 4.0, which you can read about at
 https://github.com/vega/vega-lite/releases/tag/v4.0.0.
 
diff --git a/doc/user_guide/large_datasets.rst b/doc/user_guide/large_datasets.rst
index 5b2e025a..08cfdfba 100644
--- a/doc/user_guide/large_datasets.rst
+++ b/doc/user_guide/large_datasets.rst
@@ -166,6 +166,7 @@ that you loose all interactivity features of Altair.
 Both renderers require you to install either the `vl-convert`_ or the `altair_saver`_ package, see :ref:`saving-png`,
 whereas `vl-convert`_ is expected to provide the better performance.
 
+.. _preaggregate-and-filter:
 Preaggregate and Filter in Pandas
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Another common approach is to perform data transformations such as aggregations
@@ -334,10 +335,48 @@ summary statistics to Altair instead of the full dataset.
 
 VegaFusion
 ~~~~~~~~~~
-If you work with large datasets and require your charts to be interactive,
-make sure to check out the `VegaFusion`_ package. It provides serverside acceleration
-for Altair charts. See its documentation on how to use it as well as the current limitations.
+`VegaFusion`_ is a third-party package that re-implements most Vega-Lite transforms for evaluation
+in the Python kernel.  This makes it possible to scale many Altair charts to millions of rows as long as
+they include some form of aggregation.
+
+VegaFusion 1.0 provides two rendering modes that are useful in different situations.
+
+Mime Renderer
+^^^^^^^^^^^^^
+The `VegaFusion mime renderer`_ is a good choice for charts that do not re-aggregate or re-filter data in response
+to selections. It is enabled with:
+
+.. code-block:: python
+
+    import vegafusion as vf
+    vf.enable()
+
+The mime renderer automates the :ref:`preaggregate-and-filter` workflow described above. Right before
+a chart is rendered, VegaFusion extracts the datasets and supported transforms and evaluates them in the
+Python kernel. It then removes any unused columns and inlines the transformed data into the chart specification
+for rendering.
+
+Charts rendered this way are self-contained and do not require the Python kernel or a custom
+notebook extension to display.  They are rendered with the same frontend functionality that
+is already used to display regular Altair charts.
+
+Widget Renderer
+^^^^^^^^^^^^^^^
+The `VegaFusion widget renderer`_ is a good choice for charts that re-aggregate or re-filter data in response
+to selections. It is enabled with:
+
+.. code-block:: python
+
+    import vegafusion as vf
+    vf.enable_widget()
+
+The widget renderer uses a Jupyter Widget extension to maintain a live connection between the displayed
+chart and the Python kernel. This makes it possible for transforms to be evaluated interactively in response to
+changes in selections. Charts rendered this way require a running Python kernel and Jupyter Widget extension to
+display.
 
 .. _VegaFusion: https://vegafusion.io
+.. _VegaFusion mime renderer: https://vegafusion.io/mime_renderer.html
+.. _VegaFusion widget renderer: https://vegafusion.io/widget_renderer.html
 .. _vl-convert: https://github.com/vega/vl-convert
 .. _altair_saver: http://github.com/altair-viz/altair_saver/
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 6099e5d3..af4b229c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.black]
 line-length = 88
-target-version = ['py36', 'py37', 'py38']
+target-version = ['py37', 'py38', 'py39', 'py310', 'py311']
 include = '\.pyi?$'
 exclude = '''
 /(
diff --git a/requirements_dev.txt b/requirements_dev.txt
index b8772d19..811ca48b 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,4 +1,4 @@
-black
+black<24
 ipython
 flake8
 pytest
diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py
index 32f667d5..e0eac6de 100644
--- a/tools/generate_schema_wrapper.py
+++ b/tools/generate_schema_wrapper.py
@@ -12,8 +12,13 @@ from urllib import request
 
 import m2r
 
-# import schemapi from here
-sys.path.insert(0, abspath(dirname(__file__)))
+
+# Add path so that schemapi can be imported from the tools folder
+current_dir = dirname(__file__)
+sys.path.insert(0, abspath(current_dir))
+# And another path so that Altair can be imported from head. This is relevant when
+# generate_api_docs is imported in the main function
+sys.path.insert(0, abspath(join(current_dir, "..")))
 from schemapi import codegen  # noqa: E402
 from schemapi.codegen import CodeSnippet  # noqa: E402
 from schemapi.utils import (  # noqa: E402
@@ -22,7 +27,6 @@ from schemapi.utils import (  # noqa: E402
     indent_arglist,
     resolve_references,
 )
-import generate_api_docs  # noqa: E402
 
 # Map of version name to github branch name.
 SCHEMA_VERSION = {
@@ -627,7 +631,13 @@ def main():
     copy_schemapi_util()
     vegalite_main(args.skip_download)
 
+    # The modules below are imported after the generation of the new schema files
+    # as these modules import Altair. This allows them to use the new changes
+    import generate_api_docs  # noqa: E402
+    import update_init_file  # noqa: E402
+
     generate_api_docs.write_api_file()
+    update_init_file.update__all__variable()
 
 
 if __name__ == "__main__":
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index d04bdf2d..190446bb 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -3,7 +3,7 @@ import contextlib
 import inspect
 import json
 import textwrap
-from typing import Any
+from typing import Any, Sequence, List
 
 import jsonschema
 import jsonschema.exceptions
@@ -64,9 +64,50 @@ def validate_jsonschema(spec, schema, rootschema=None):
     if hasattr(validator_cls, "FORMAT_CHECKER"):
         validator_kwargs["format_checker"] = validator_cls.FORMAT_CHECKER
     validator = validator_cls(schema, **validator_kwargs)
-    error = jsonschema.exceptions.best_match(validator.iter_errors(spec))
-    if error is not None:
-        raise error
+    errors = list(validator.iter_errors(spec))
+    if errors:
+        errors = _get_most_relevant_errors(errors)
+        main_error = errors[0]
+        # They can be used to craft more helpful error messages when this error
+        # is being converted to a SchemaValidationError
+        main_error._additional_errors = errors[1:]
+        raise main_error
+
+
+def _get_most_relevant_errors(
+    errors: Sequence[jsonschema.exceptions.ValidationError],
+) -> List[jsonschema.exceptions.ValidationError]:
+    if len(errors) == 0:
+        return []
+
+    # Go to lowest level in schema where an error happened as these give
+    # the most relevant error messages
+    lowest_level = errors[0]
+    while lowest_level.context:
+        lowest_level = lowest_level.context[0]
+
+    most_relevant_errors = []
+    if lowest_level.validator == "additionalProperties":
+        # For these errors, the message is already informative enough and the other
+        # errors on the lowest level are not helpful for a user but instead contain
+        # the same information in a more verbose way
+        most_relevant_errors = [lowest_level]
+    else:
+        parent = lowest_level.parent
+        if parent is None:
+            # In this case we are still at the top level and can return all errors
+            most_relevant_errors = errors
+        else:
+            # Return all errors of the lowest level out of which
+            # we can construct more informative error messages
+            most_relevant_errors = lowest_level.parent.context
+
+    # This should never happen but might still be good to test for it as else
+    # the original error would just slip through without being raised
+    if len(most_relevant_errors) == 0:
+        raise Exception("Could not determine the most relevant errors") from errors[0]
+
+    return most_relevant_errors
 
 
 def _subclasses(cls):
@@ -80,18 +121,14 @@ def _subclasses(cls):
             yield cls
 
 
-def _todict(obj, validate, context):
+def _todict(obj, context):
     """Convert an object to a dict representation."""
     if isinstance(obj, SchemaBase):
-        return obj.to_dict(validate=validate, context=context)
+        return obj.to_dict(validate=False, context=context)
     elif isinstance(obj, (list, tuple, np.ndarray)):
-        return [_todict(v, validate, context) for v in obj]
+        return [_todict(v, context) for v in obj]
     elif isinstance(obj, dict):
-        return {
-            k: _todict(v, validate, context)
-            for k, v in obj.items()
-            if v is not Undefined
-        }
+        return {k: _todict(v, context) for k, v in obj.items() if v is not Undefined}
     elif hasattr(obj, "to_dict"):
         return obj.to_dict()
     elif isinstance(obj, np.number):
@@ -115,24 +152,9 @@ class SchemaValidationError(jsonschema.ValidationError):
     """A wrapper for jsonschema.ValidationError with friendlier traceback"""
 
     def __init__(self, obj, err):
-        super(SchemaValidationError, self).__init__(**self._get_contents(err))
+        super(SchemaValidationError, self).__init__(**err._contents())
         self.obj = obj
-
-    @staticmethod
-    def _get_contents(err):
-        """Get a dictionary with the contents of a ValidationError"""
-        try:
-            # works in jsonschema 2.3 or later
-            contents = err._contents()
-        except AttributeError:
-            try:
-                # works in Python >=3.4
-                spec = inspect.getfullargspec(err.__init__)
-            except AttributeError:
-                # works in Python <3.4
-                spec = inspect.getargspec(err.__init__)
-            contents = {key: getattr(err, key) for key in spec.args[1:]}
-        return contents
+        self._additional_errors = getattr(err, "_additional_errors", [])
 
     def __str__(self):
         cls = self.obj.__class__
@@ -143,13 +165,18 @@ class SchemaValidationError(jsonschema.ValidationError):
             for val in schema_path[:-1]
             if val not in ("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}
 
         {}
         """.format(
-            schema_path, self.validator, self.message
+            schema_path, self.validator, message
         )
 
 
@@ -325,11 +352,9 @@ class SchemaBase(object):
 
         Parameters
         ----------
-        validate : boolean or string
+        validate : boolean
             If True (default), then validate the output dictionary
-            against the schema. If "deep" then recursively validate
-            all objects in the spec. This takes much more time, but
-            it results in friendlier tracebacks for large objects.
+            against the schema.
         ignore : list
             A list of keys to ignore. This will *not* passed to child to_dict
             function calls.
@@ -351,10 +376,9 @@ class SchemaBase(object):
             context = {}
         if ignore is None:
             ignore = []
-        sub_validate = "deep" if validate == "deep" else False
 
         if self._args and not self._kwds:
-            result = _todict(self._args[0], validate=sub_validate, context=context)
+            result = _todict(self._args[0], context=context)
         elif not self._args:
             kwds = self._kwds.copy()
             # parsed_shorthand is added by FieldChannelMixin.
@@ -382,7 +406,6 @@ class SchemaBase(object):
             }
             result = _todict(
                 kwds,
-                validate=sub_validate,
                 context=context,
             )
         else:
@@ -404,11 +427,9 @@ class SchemaBase(object):
 
         Parameters
         ----------
-        validate : boolean or string
+        validate : boolean
             If True (default), then validate the output dictionary
-            against the schema. If "deep" then recursively validate
-            all objects in the spec. This takes much more time, but
-            it results in friendlier tracebacks for large objects.
+            against the schema.
         ignore : list
             A list of keys to ignore. This will *not* passed to child to_dict
             function calls.
@@ -514,7 +535,7 @@ class SchemaBase(object):
         Validate a property against property schema in the context of the
         rootschema
         """
-        value = _todict(value, validate=False, context={})
+        value = _todict(value, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
         return validate_jsonschema(
             value, props.get(name, {}), rootschema=cls._rootschema or cls._schema
diff --git a/tools/schemapi/utils.py b/tools/schemapi/utils.py
index 61261773..59fa14e2 100644
--- a/tools/schemapi/utils.py
+++ b/tools/schemapi/utils.py
@@ -192,7 +192,6 @@ class SchemaInfo(object):
 
     @property
     def medium_description(self):
-
         if self.is_list():
             return "[{0}]".format(
                 ", ".join(self.child(s).short_description for s in self.schema)
diff --git a/tools/update_init_file.py b/tools/update_init_file.py
new file mode 100644
index 00000000..921715a5
--- /dev/null
+++ b/tools/update_init_file.py
@@ -0,0 +1,77 @@
+"""
+This script updates the attribute __all__ in altair/__init__.py
+based on the updated Altair schema.
+"""
+import inspect
+import sys
+from pathlib import Path
+from os.path import abspath, dirname, join
+
+import black
+
+# Import Altair from head
+ROOT_DIR = abspath(join(dirname(__file__), ".."))
+sys.path.insert(0, ROOT_DIR)
+import altair as alt  # noqa: E402
+
+
+def update__all__variable():
+    """Updates the __all__ variable to all relevant attributes of top-level Altair.
+    This is for example useful to hide deprecated attributes from code completion in
+    Jupyter.
+    """
+    # Read existing file content
+    init_path = alt.__file__
+    with open(init_path, "r") as f:
+        lines = f.readlines()
+    lines = [line.strip("\n") for line in lines]
+
+    # Find first and last line of the definition of __all__
+    first_definition_line = None
+    last_definition_line = None
+    for idx, line in enumerate(lines):
+        if line.startswith("__all__ ="):
+            first_definition_line = idx
+        elif first_definition_line is not None and line.startswith("]"):
+            last_definition_line = idx
+            break
+    assert first_definition_line is not None and last_definition_line is not None
+
+    # Figure out which attributes are relevant
+    relevant_attributes = [x for x in alt.__dict__ if _is_relevant_attribute(x)]
+    relevant_attributes.sort()
+    relevant_attributes_str = f"__all__ = {relevant_attributes}"
+
+    # Put file back together, replacing old definition of __all__ with new one, keeping
+    # the rest of the file as is
+    new_lines = (
+        lines[:first_definition_line]
+        + [relevant_attributes_str]
+        + lines[last_definition_line + 1 :]
+    )
+    # Format file content with black
+    new_file_content = black.format_str("\n".join(new_lines), mode=black.Mode())
+
+    # Write new version of altair/__init__.py
+    with open(init_path, "w") as f:
+        f.write(new_file_content)
+
+
+def _is_relevant_attribute(attr_name):
+    attr = getattr(alt, attr_name)
+    if getattr(attr, "_deprecated", False) is True or attr_name.startswith("_"):
+        return False
+    else:
+        if inspect.ismodule(attr):
+            # Only include modules which are part of Altair. This excludes built-in
+            # modules (they do not have a __file__ attribute), standard library,
+            # and third-party packages.
+            return getattr(attr, "__file__", "").startswith(
+                str(Path(alt.__file__).parent)
+            )
+        else:
+            return True
+
+
+if __name__ == "__main__":
+    update__all__variable()


Test patch:
diff --git a/tests/test_toplevel.py b/tests/test_toplevel.py
new file mode 100644
index 00000000..0da425ad
--- /dev/null
+++ b/tests/test_toplevel.py
@@ -0,0 +1,20 @@
+import sys
+from os.path import abspath, join, dirname
+
+import altair as alt
+
+current_dir = dirname(__file__)
+sys.path.insert(0, abspath(join(current_dir, "..")))
+from tools import update_init_file  # noqa: E402
+
+
+def test_completeness_of__all__():
+    relevant_attributes = [
+        x for x in alt.__dict__ if update_init_file._is_relevant_attribute(x)
+    ]
+    relevant_attributes.sort()
+
+    # If the assert statement fails below, there are probably either new objects
+    # in the top-level Altair namespace or some were removed.
+    # In that case, run tools/update_init_file.py to update __all__
+    assert getattr(alt, "__all__") == relevant_attributes
diff --git a/tests/utils/tests/test_schemapi.py b/tests/utils/tests/test_schemapi.py
index 4c82839b..d7263404 100644
--- a/tests/utils/tests/test_schemapi.py
+++ b/tests/utils/tests/test_schemapi.py
@@ -4,11 +4,14 @@ import copy
 import io
 import json
 import jsonschema
+import re
 import pickle
-import pytest
+import warnings
 
 import numpy as np
 import pandas as pd
+import pytest
+from vega_datasets import data
 
 import altair as alt
 from altair import load_schema
@@ -380,6 +383,131 @@ def test_schema_validation_error():
     assert the_err.message in message
 
 
+def chart_example_layer():
+    points = (
+        alt.Chart(data.cars.url)
+        .mark_point()
+        .encode(
+            x="Horsepower:Q",
+            y="Miles_per_Gallon:Q",
+        )
+    )
+    return (points & points).properties(width=400)
+
+
+def chart_example_hconcat():
+    source = data.cars()
+    points = (
+        alt.Chart(source)
+        .mark_point()
+        .encode(
+            x="Horsepower",
+            y="Miles_per_Gallon",
+        )
+    )
+
+    text = (
+        alt.Chart(source)
+        .mark_text(align="right")
+        .encode(alt.Text("Horsepower:N", title=dict(text="Horsepower", align="right")))
+    )
+
+    return points | text
+
+
+def chart_example_invalid_channel_and_condition():
+    selection = alt.selection_point()
+    return (
+        alt.Chart(data.barley())
+        .mark_circle()
+        .add_params(selection)
+        .encode(
+            color=alt.condition(selection, alt.value("red"), alt.value("green")),
+            invalidChannel=None,
+        )
+    )
+
+
+def chart_example_invalid_y_option():
+    return (
+        alt.Chart(data.barley())
+        .mark_bar()
+        .encode(
+            x=alt.X("variety", unknown=2),
+            y=alt.Y("sum(yield)", stack="asdf"),
+        )
+    )
+
+
+def chart_example_invalid_y_option_value():
+    return (
+        alt.Chart(data.barley())
+        .mark_bar()
+        .encode(
+            x=alt.X("variety"),
+            y=alt.Y("sum(yield)", stack="asdf"),
+        )
+    )
+
+
+def chart_example_invalid_y_option_value_with_condition():
+    return (
+        alt.Chart(data.barley())
+        .mark_bar()
+        .encode(
+            x="variety",
+            y=alt.Y("sum(yield)", stack="asdf"),
+            opacity=alt.condition("datum.yield > 0", alt.value(1), alt.value(0.2)),
+        )
+    )
+
+
+@pytest.mark.parametrize(
+    "chart_func, expected_error_message",
+    [
+        (
+            chart_example_invalid_y_option,
+            r"Additional properties are not allowed \('unknown' was unexpected\)",
+        ),
+        (
+            chart_example_invalid_y_option_value,
+            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,
+            r"Additional properties are not allowed \('width' was unexpected\)",
+        ),
+        (
+            chart_example_invalid_y_option_value_with_condition,
+            r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
+            + r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
+        ),
+        (
+            chart_example_hconcat,
+            r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'string'.*"
+            + r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'array'",
+        ),
+        (
+            chart_example_invalid_channel_and_condition,
+            r"Additional properties are not allowed \('invalidChannel' was unexpected\)",
+        ),
+    ],
+)
+def test_chart_validation_errors(chart_func, expected_error_message):
+    # DOTALL flag makes that a dot (.) also matches new lines
+    pattern = re.compile(expected_error_message, re.DOTALL)
+    # For some wrong chart specifications such as an unknown encoding channel,
+    # Altair already raises a warning before the chart specifications are validated.
+    # We can ignore these warnings as we are interested in the errors being raised
+    # during validation which is triggered by to_dict
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", category=UserWarning)
+        chart = chart_func()
+    with pytest.raises(SchemaValidationError, match=pattern):
+        chart.to_dict()
+
+
 def test_serialize_numpy_types():
     m = MySchema(
         a={"date": np.datetime64("2019-01-01")},
diff --git a/tests/utils/tests/test_utils.py b/tests/utils/tests/test_utils.py
index 9f31ae4f..690fdc85 100644
--- a/tests/utils/tests/test_utils.py
+++ b/tests/utils/tests/test_utils.py
@@ -116,7 +116,6 @@ def test_sanitize_dataframe_infs():
     reason="Nullable integers not supported in pandas v{}".format(pd.__version__),
 )
 def test_sanitize_nullable_integers():
-
     df = pd.DataFrame(
         {
             "int_np": [1, 2, 3, 4, 5],
diff --git a/tests/vegalite/v5/tests/test_api.py b/tests/vegalite/v5/tests/test_api.py
index 12e8b666..b4b82a17 100644
--- a/tests/vegalite/v5/tests/test_api.py
+++ b/tests/vegalite/v5/tests/test_api.py
@@ -944,17 +944,33 @@ def test_layer_errors():
         'Objects with "config" attribute cannot be used within LayerChart.'
     )
 
+    with pytest.raises(ValueError) as err:
+        alt.hconcat(simple_chart) + simple_chart
+    assert (
+        str(err.value)
+        == "Concatenated charts cannot be layered. Instead, layer the charts before concatenating."
+    )
+
     with pytest.raises(ValueError) as err:
         repeat_chart + simple_chart
-    assert str(err.value) == "Repeat charts cannot be layered."
+    assert (
+        str(err.value)
+        == "Repeat charts cannot be layered. Instead, layer the charts before repeating."
+    )
 
     with pytest.raises(ValueError) as err:
         facet_chart1 + simple_chart
-    assert str(err.value) == "Faceted charts cannot be layered."
+    assert (
+        str(err.value)
+        == "Faceted charts cannot be layered. Instead, layer the charts before faceting."
+    )
 
     with pytest.raises(ValueError) as err:
         alt.layer(simple_chart) + facet_chart2
-    assert str(err.value) == "Faceted charts cannot be layered."
+    assert (
+        str(err.value)
+        == "Faceted charts cannot be layered. Instead, layer the charts before faceting."
+    )
 
 
 @pytest.mark.parametrize(
diff --git a/tools/schemapi/tests/test_schemapi.py b/tools/schemapi/tests/test_schemapi.py
index f5e7469e..d26b127f 100644
--- a/tools/schemapi/tests/test_schemapi.py
+++ b/tools/schemapi/tests/test_schemapi.py
@@ -2,11 +2,14 @@ import copy
 import io
 import json
 import jsonschema
+import re
 import pickle
-import pytest
+import warnings
 
 import numpy as np
 import pandas as pd
+import pytest
+from vega_datasets import data
 
 import altair as alt
 from altair import load_schema
@@ -378,6 +381,131 @@ def test_schema_validation_error():
     assert the_err.message in message
 
 
+def chart_example_layer():
+    points = (
+        alt.Chart(data.cars.url)
+        .mark_point()
+        .encode(
+            x="Horsepower:Q",
+            y="Miles_per_Gallon:Q",
+        )
+    )
+    return (points & points).properties(width=400)
+
+
+def chart_example_hconcat():
+    source = data.cars()
+    points = (
+        alt.Chart(source)
+        .mark_point()
+        .encode(
+            x="Horsepower",
+            y="Miles_per_Gallon",
+        )
+    )
+
+    text = (
+        alt.Chart(source)
+        .mark_text(align="right")
+        .encode(alt.Text("Horsepower:N", title=dict(text="Horsepower", align="right")))
+    )
+
+    return points | text
+
+
+def chart_example_invalid_channel_and_condition():
+    selection = alt.selection_point()
+    return (
+        alt.Chart(data.barley())
+        .mark_circle()
+        .add_params(selection)
+        .encode(
+            color=alt.condition(selection, alt.value("red"), alt.value("green")),
+            invalidChannel=None,
+        )
+    )
+
+
+def chart_example_invalid_y_option():
+    return (
+        alt.Chart(data.barley())
+        .mark_bar()
+        .encode(
+            x=alt.X("variety", unknown=2),
+            y=alt.Y("sum(yield)", stack="asdf"),
+        )
+    )
+
+
+def chart_example_invalid_y_option_value():
+    return (
+        alt.Chart(data.barley())
+        .mark_bar()
+        .encode(
+            x=alt.X("variety"),
+            y=alt.Y("sum(yield)", stack="asdf"),
+        )
+    )
+
+
+def chart_example_invalid_y_option_value_with_condition():
+    return (
+        alt.Chart(data.barley())
+        .mark_bar()
+        .encode(
+            x="variety",
+            y=alt.Y("sum(yield)", stack="asdf"),
+            opacity=alt.condition("datum.yield > 0", alt.value(1), alt.value(0.2)),
+        )
+    )
+
+
+@pytest.mark.parametrize(
+    "chart_func,expected_error_message",
+    [
+        (
+            chart_example_invalid_y_option,
+            r"Additional properties are not allowed \('unknown' was unexpected\)",
+        ),
+        (
+            chart_example_invalid_y_option_value,
+            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,
+            r"Additional properties are not allowed \('width' was unexpected\)",
+        ),
+        (
+            chart_example_invalid_y_option_value_with_condition,
+            r"'asdf' is not one of \['zero', 'center', 'normalize'\].*"
+            + r"'asdf' is not of type 'null'.*'asdf' is not of type 'boolean'",
+        ),
+        (
+            chart_example_hconcat,
+            r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'string'.*"
+            + r"\{'text': 'Horsepower', 'align': 'right'\} is not of type 'array'",
+        ),
+        (
+            chart_example_invalid_channel_and_condition,
+            r"Additional properties are not allowed \('invalidChannel' was unexpected\)",
+        ),
+    ],
+)
+def test_chart_validation_errors(chart_func, expected_error_message):
+    # DOTALL flag makes that a dot (.) also matches new lines
+    pattern = re.compile(expected_error_message, re.DOTALL)
+    # For some wrong chart specifications such as an unknown encoding channel,
+    # Altair already raises a warning before the chart specifications are validated.
+    # We can ignore these warnings as we are interested in the errors being raised
+    # during validation which is triggered by to_dict
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", category=UserWarning)
+        chart = chart_func()
+    with pytest.raises(SchemaValidationError, match=pattern):
+        chart.to_dict()
+
+
 def test_serialize_numpy_types():
     m = MySchema(
         a={"date": np.datetime64("2019-01-01")},