Repository: altair-viz/altair
Pull request #2807 (2023-01-05 15:10:49)

Add inline argument to chart.save() for html export
This includes the Vega/Vega-Lite/Vega-Embed JavaScript source in the exported html file so that it works offline.  The logic is ported from altair_saver.

See https://github.com/altair-viz/altair/issues/2765

**Note:** Like altair_saver, this pulls the JavaScript source from the `altair_viewer` package. In the future we could talk about whether to include the JavaScript bundles in Altair itself to avoid this dependency.

jonmmease: Seeing a couple of CI errors with altair_saver and Python 3.11. Does this look familiar to anyone? Should be try kicking off the tests again?
jonmmease: Also, I tested this manually. But it would be good for someone else to try it out manually as well since the test case for checking that it's working just checks for an expected substring that's different that what's expected with `inline=False`.

Patch:
diff --git a/.github/workflows/docbuild.yml b/.github/workflows/docbuild.yml
index 788deac1..18ac20a0 100644
--- a/.github/workflows/docbuild.yml
+++ b/.github/workflows/docbuild.yml
@@ -19,10 +19,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install .[dev]
-        pip install "selenium<4.3.0"
-        pip install altair_saver
         pip install -r doc/requirements.txt
-        pip uninstall vl-convert-python --yes
         pip install git+https://github.com/altair-viz/altair_viewer.git
     - name: Run docbuild
       run: |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 59de7e4f..2e81ce75 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -152,6 +152,9 @@ Some additional notes:
   included then it should be referenced by URL, such as `source =
   data.movies.url`. This is to ensure that Altair's automated test suite does
   not depend on availability of external HTTP resources.
+- If VlConvert does not support PNG export of the chart (e.g. in the case of emoji),
+  then add the name of the example to the `SVG_EXAMPLES` set in 
+  `tests/examples_arguments_syntax/__init__.py` and `tests/examples_methods_syntax/__init__.py`
 
 ### Building the Documentation Locally
 In addition to the development dependencies mentioned further above,
@@ -164,12 +167,6 @@ for details.
 pip install -r doc/requirements.txt
 ```
 
-If you want to build the documentation so it can be published on the [official Altair website](https://altair-viz.github.io), you need to
-additionally install [altair_saver](https://github.com/altair-viz/altair_saver/) and uninstall [vl-convert-python](https://github.com/vega/vl-convert/tree/main/vl-convert-python).
-These packages are used to generate PNG thumbnails for the example gallery. If both are installed, `vl-convert-python` is used by default.
-The reason for this is that `vl-convert-python` cannot create a thumbnail for the `isotype_emoji.py` example,
-which is fine for normal contributions but not when creating a new version of the official documentation.
-
 Once you have all the dependencies, you can build the documentation 
 using various commands defined in the Makefile. 
 From the `doc` folder, you can use `make help` to see all of the available commands
diff --git a/Makefile b/Makefile
index bacc76e3..ed7b5a0f 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ install:
 	python setup.py install
 
 test :
-	black --diff --color .
+	black --diff --color --check .
 	flake8 . --statistics
 	python -m pytest --pyargs --doctest-modules tests
 
diff --git a/altair/utils/display.py b/altair/utils/display.py
index bcf8232b..50c87b10 100644
--- a/altair/utils/display.py
+++ b/altair/utils/display.py
@@ -125,7 +125,10 @@ class Displayable(object):
         # type: () -> None
         """Validate the spec against the schema."""
         schema_dict = json.loads(pkgutil.get_data(*self.schema_path).decode("utf-8"))
-        validate_jsonschema(self.spec, schema_dict)
+        validate_jsonschema(
+            self.spec,
+            schema_dict,
+        )
 
     def _repr_mimebundle_(self, include=None, exclude=None):
         """Return a MIME bundle for display in Jupyter frontends."""
diff --git a/altair/utils/html.py b/altair/utils/html.py
index de2b421f..6be18139 100644
--- a/altair/utils/html.py
+++ b/altair/utils/html.py
@@ -141,9 +141,42 @@ HTML_TEMPLATE_UNIVERSAL = jinja2.Template(
 )
 
 
+# This is like the HTML_TEMPLATE template, but includes vega javascript inline
+# so that the resulting file is not dependent on external resources. This was
+# ported over from altair_saver.
+#
+# implies requirejs=False and full_html=True
+INLINE_HTML_TEMPLATE = jinja2.Template(
+    """\
+<!DOCTYPE html>
+<html>
+<head>
+  <script type="text/javascript">
+    // vega.js v{{ vega_version }}
+    {{ vega_script }}
+    // vega-lite.js v{{ vegalite_version }}
+    {{ vegalite_script }}
+    // vega-embed.js v{{ vegaembed_version }}
+    {{ vegaembed_script }}
+  </script>
+</head>
+<body>
+<div class="vega-visualization" id="{{ output_div }}"></div>
+<script type="text/javascript">
+  const spec = {{ spec }};
+  const embedOpt = {{ embed_options }};
+  vegaEmbed('#{{ output_div }}', spec, embedOpt).catch(console.error);
+</script>
+</body>
+</html>
+"""
+)
+
+
 TEMPLATES = {
     "standard": HTML_TEMPLATE,
     "universal": HTML_TEMPLATE_UNIVERSAL,
+    "inline": INLINE_HTML_TEMPLATE,
 }
 
 
@@ -218,6 +251,22 @@ def spec_to_html(
     if mode == "vega-lite" and vegalite_version is None:
         raise ValueError("must specify vega-lite version for mode='vega-lite'")
 
+    render_kwargs = dict()
+    if template == "inline":
+        try:
+            from altair_viewer import get_bundled_script
+        except ImportError:
+            raise ImportError(
+                "The altair_viewer package is required to convert to HTML with inline=True"
+            )
+        render_kwargs["vega_script"] = get_bundled_script("vega", vega_version)
+        render_kwargs["vegalite_script"] = get_bundled_script(
+            "vega-lite", vegalite_version
+        )
+        render_kwargs["vegaembed_script"] = get_bundled_script(
+            "vega-embed", vegaembed_version
+        )
+
     template = TEMPLATES.get(template, template)
     if not hasattr(template, "render"):
         raise ValueError("Invalid template: {0}".format(template))
@@ -233,4 +282,5 @@ def spec_to_html(
         output_div=output_div,
         fullhtml=fullhtml,
         requirejs=requirejs,
+        **render_kwargs,
     )
diff --git a/altair/utils/save.py b/altair/utils/save.py
index fd6fccb5..3ba845b0 100644
--- a/altair/utils/save.py
+++ b/altair/utils/save.py
@@ -1,5 +1,6 @@
 import json
 import pathlib
+import warnings
 
 from .mimebundle import spec_to_mimebundle
 
@@ -27,11 +28,12 @@ def save(
     webdriver=None,
     scale_factor=1,
     engine=None,
+    inline=False,
     **kwargs,
 ):
     """Save a chart to file in a variety of formats
 
-    Supported formats are [json, html, png, svg]
+    Supported formats are [json, html, png, svg, pdf]
 
     Parameters
     ----------
@@ -40,7 +42,7 @@ def save(
     fp : string filename, pathlib.Path or file-like object
         file to which to write the chart.
     format : string (optional)
-        the format to write: one of ['json', 'html', 'png', 'svg'].
+        the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
         If not specified, the format will be determined from the filename.
     mode : string (optional)
         Either 'vega' or 'vegalite'. If not specified, then infer the mode from
@@ -64,6 +66,12 @@ def save(
         scale_factor to use to change size/resolution of png or svg output
     engine: string {'vl-convert', 'altair_saver'}
         the conversion engine to use for 'png', 'svg', and 'pdf' formats
+    inline: bool (optional)
+        If False (default), the required JavaScript libraries are loaded
+        from a CDN location in the resulting html file.
+        If True, the required JavaScript libraries are inlined into the resulting
+        html file so that it will work without an internet connection.
+        The altair_viewer package is required if True.
     **kwargs :
         additional kwargs passed to spec_to_mimebundle.
     """
@@ -99,10 +107,15 @@ def save(
     if mode == "vega-lite" and vegalite_version is None:
         raise ValueError("must specify vega-lite version")
 
+    if format != "html" and inline:
+        warnings.warn("inline argument ignored for non HTML formats.")
+
     if format == "json":
         json_spec = json.dumps(spec, **json_kwds)
         write_file_or_filename(fp, json_spec, mode="w")
     elif format == "html":
+        if inline:
+            kwargs["template"] = "inline"
         mimebundle = spec_to_mimebundle(
             spec=spec,
             format=format,
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index 2c86b1d2..b94017c9 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -9,12 +9,12 @@ from typing import Any
 
 import jsonschema
 import jsonschema.exceptions
+import jsonschema.validators
 import numpy as np
 import pandas as pd
 
 from altair import vegalite
 
-JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator
 # 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.
@@ -44,7 +44,7 @@ def debug_mode(arg):
         DEBUG_MODE = original
 
 
-def validate_jsonschema(spec, schema, resolver=None):
+def validate_jsonschema(spec, schema, rootschema=None):
     # We don't use jsonschema.validate as this would validate the schema itself.
     # Instead, we pass the schema directly to the validator class. This is done for
     # two reasons: The schema comes from Vega-Lite and is not based on the user
@@ -54,9 +54,18 @@ def validate_jsonschema(spec, schema, resolver=None):
     # e.g. '#/definitions/ValueDefWithCondition<MarkPropFieldOrDatumDef,
     # (Gradient|string|null)>' would be a valid $ref in a Vega-Lite schema but
     # it is not a valid URI reference due to the characters such as '<'.
-    validator = JSONSCHEMA_VALIDATOR(
-        schema, format_checker=JSONSCHEMA_VALIDATOR.FORMAT_CHECKER, resolver=resolver
-    )
+    if rootschema is not None:
+        validator_cls = jsonschema.validators.validator_for(rootschema)
+        resolver = jsonschema.RefResolver.from_schema(rootschema)
+    else:
+        validator_cls = jsonschema.validators.validator_for(schema)
+        # No resolver is necessary if the schema is already the full schema
+        resolver = None
+
+    validator_kwargs = {"resolver": resolver}
+    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
@@ -177,7 +186,6 @@ class SchemaBase(object):
     _schema = None
     _rootschema = None
     _class_is_valid_at_instantiation = True
-    _validator = JSONSCHEMA_VALIDATOR
 
     def __init__(self, *args, **kwds):
         # Two valid options for initialization, which should be handled by
@@ -466,8 +474,9 @@ class SchemaBase(object):
         """
         if schema is None:
             schema = cls._schema
-        resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return validate_jsonschema(instance, schema, resolver=resolver)
+        return validate_jsonschema(
+            instance, schema, rootschema=cls._rootschema or cls._schema
+        )
 
     @classmethod
     def resolve_references(cls, schema=None):
@@ -485,8 +494,9 @@ class SchemaBase(object):
         """
         value = _todict(value, validate=False, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
-        resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return validate_jsonschema(value, props.get(name, {}), resolver=resolver)
+        return validate_jsonschema(
+            value, props.get(name, {}), rootschema=cls._rootschema or cls._schema
+        )
 
     def __dir__(self):
         return list(self._kwds.keys())
@@ -578,9 +588,8 @@ class _FromDict(object):
         if "anyOf" in schema or "oneOf" in schema:
             schemas = schema.get("anyOf", []) + schema.get("oneOf", [])
             for possible_schema in schemas:
-                resolver = jsonschema.RefResolver.from_schema(rootschema)
                 try:
-                    validate_jsonschema(dct, possible_schema, resolver=resolver)
+                    validate_jsonschema(dct, possible_schema, rootschema=rootschema)
                 except jsonschema.ValidationError:
                     continue
                 else:
diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py
index 84a8018d..e5bd2cc8 100644
--- a/altair/vegalite/v5/api.py
+++ b/altair/vegalite/v5/api.py
@@ -3042,14 +3042,14 @@ def _remove_layer_props(chart, subcharts, layer_props):
                     pass
             if len(values) == 0:
                 pass
-            elif all(v is values[0] for v in values[1:]):
+            elif all(v == values[0] for v in values[1:]):
                 output_dict[prop] = values[0]
             else:
                 raise ValueError(f"There are inconsistent values {values} for {prop}")
         else:
             # Top level has this prop; subchart props must be either
             # Undefined or identical to proceed.
-            if all(c[prop] is Undefined or c[prop] is chart[prop] for c in subcharts):
+            if all(c[prop] is Undefined or c[prop] == chart[prop] for c in subcharts):
                 output_dict[prop] = chart[prop]
             else:
                 raise ValueError(f"There are inconsistent values {values} for {prop}")
diff --git a/altair/vegalite/v5/theme.py b/altair/vegalite/v5/theme.py
index 1f88f969..ba396cb3 100644
--- a/altair/vegalite/v5/theme.py
+++ b/altair/vegalite/v5/theme.py
@@ -22,7 +22,7 @@ class VegaTheme(object):
     def __call__(self):
         return {
             "usermeta": {"embedOptions": {"theme": self.theme}},
-            "config": {"view": {"continuousWidth": 400, "continuousHeight": 300}},
+            "config": {"view": {"continuousWidth": 300, "continuousHeight": 300}},
         }
 
     def __repr__(self):
@@ -37,14 +37,14 @@ themes = ThemeRegistry(entry_point_group=ENTRY_POINT_GROUP)
 
 themes.register(
     "default",
-    lambda: {"config": {"view": {"continuousWidth": 400, "continuousHeight": 300}}},
+    lambda: {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}},
 )
 themes.register(
     "opaque",
     lambda: {
         "config": {
             "background": "white",
-            "view": {"continuousWidth": 400, "continuousHeight": 300},
+            "view": {"continuousWidth": 300, "continuousHeight": 300},
         }
     },
 )
diff --git a/doc/user_guide/customization.rst b/doc/user_guide/customization.rst
index 0c2ac5d5..a0186603 100644
--- a/doc/user_guide/customization.rst
+++ b/doc/user_guide/customization.rst
@@ -582,7 +582,7 @@ For example, the default theme configures the default size of a single chart:
     >>> import altair as alt
     >>> default = alt.themes.get()
     >>> default()
-    {'config': {'view': {'continuousWidth': 400, 'continuousHeight': 300}}}
+    {'config': {'view': {'continuousWidth': 300, 'continuousHeight': 300}}}
 
 You can see that any chart you create will have this theme applied, and these configurations
 added to its specification:
@@ -673,7 +673,7 @@ fill unless otherwise specified:
             'config': {
                 'view': {
                     'height': 300,
-                    'width': 400,
+                    'width': 300,
                 },
                 'mark': {
                     'color': 'black',
diff --git a/doc/user_guide/internals.rst b/doc/user_guide/internals.rst
index 104a6d4f..f63479ad 100644
--- a/doc/user_guide/internals.rst
+++ b/doc/user_guide/internals.rst
@@ -38,7 +38,7 @@ from which we can output the JSON representation:
         color='Origin:N',
     ).configure_view(
         continuousHeight=300,
-        continuousWidth=400,
+        continuousWidth=300,
     )
 
     print(chart.to_json(indent=2))
@@ -98,7 +98,7 @@ the above chart using these low-level object types directly:
         config=alt.Config(
             view=alt.ViewConfig(
                 continuousHeight=300,
-                continuousWidth=400
+                continuousWidth=300
             )
         )
     )
diff --git a/doc/user_guide/large_datasets.rst b/doc/user_guide/large_datasets.rst
index 4c9e6348..5b2e025a 100644
--- a/doc/user_guide/large_datasets.rst
+++ b/doc/user_guide/large_datasets.rst
@@ -54,7 +54,7 @@ simple chart made from a dataframe with three rows of data:
 .. code-block:: none
 
     {'$schema': 'https://vega.github.io/schema/vega-lite/v2.4.1.json',
-     'config': {'view': {'height': 300, 'width': 400}},
+     'config': {'view': {'height': 300, 'width': 300}},
      'data': {'values': [{'x': 1, 'y': 2}, {'x': 2, 'y': 1}, {'x': 3, 'y': 2}]},
      'encoding': {'x': {'field': 'x', 'type': 'quantitative'},
                   'y': {'field': 'y', 'type': 'quantitative'}},
@@ -119,7 +119,7 @@ You can also persist the data to disk and then pass the path to Altair:
 .. code-block:: none
 
     {'$schema': 'https://vega.github.io/schema/vega-lite/v2.4.1.json',
-     'config': {'view': {'height': 300, 'width': 400}},
+     'config': {'view': {'height': 300, 'width': 300}},
      'data': {'url': 'data.json'},
      'encoding': {'x': {'field': 'x', 'type': 'quantitative'},
                   'y': {'field': 'y', 'type': 'quantitative'}},
diff --git a/doc/user_guide/saving_charts.rst b/doc/user_guide/saving_charts.rst
index aa3ad09a..ec0d5939 100644
--- a/doc/user_guide/saving_charts.rst
+++ b/doc/user_guide/saving_charts.rst
@@ -41,7 +41,7 @@ The contents of the resulting file will look something like this:
       "config": {
         "view": {
           "continuousHeight": 300,
-          "continuousWidth": 400
+          "continuousWidth": 300
         }
       },
       "data": {
@@ -90,7 +90,7 @@ javascript-enabled web browser:
     <html>
     <head>
       <script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
-      <script src="https://cdn.jsdelivr.net/npm/vega-lite@4"></script>
+      <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
       <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
     </head>
     <body>
@@ -101,7 +101,7 @@ javascript-enabled web browser:
           "config": {
             "view": {
               "continuousHeight": 300,
-              "continuousWidth": 400
+              "continuousWidth": 300
             }
           },
           "data": {
diff --git a/sphinxext/altairgallery.py b/sphinxext/altairgallery.py
index 2b62dcba..268868d7 100644
--- a/sphinxext/altairgallery.py
+++ b/sphinxext/altairgallery.py
@@ -5,6 +5,7 @@ import random
 import collections
 from operator import itemgetter
 import warnings
+import shutil
 
 import jinja2
 
@@ -59,7 +60,15 @@ Many draw upon sample datasets compiled by the `Vega <https://vega.github.io/veg
    <span class="gallery">
    {% for example in group %}
    <a class="imagegroup" href="{{ example.name }}.html">
-     <span class="image" alt="{{ example.title }}" style="background-image: url(..{{ image_dir }}/{{ example.name }}-thumb.png);"></span>
+   <span
+        class="image" alt="{{ example.title }}"
+{% if example['use_svg'] %}
+        style="background-image: url(..{{ image_dir }}/{{ example.name }}-thumb.svg);"
+{% else %}
+        style="background-image: url(..{{ image_dir }}/{{ example.name }}-thumb.png);"
+{% endif %}
+    ></span>
+
      <span class="image-title">{{ example.title }}</span>
    </a>
    {% endfor %}
@@ -87,7 +96,14 @@ MINIGALLERY_TEMPLATE = jinja2.Template(
     <div id="showcase">
       <div class="examples">
       {% for example in examples %}
-        <a class="preview" href="{{ gallery_dir }}/{{ example.name }}.html" style="background-image: url(.{{ image_dir }}/{{ example.name }}-thumb.png)"></a>
+      <a
+        class="preview" href="{{ gallery_dir }}/{{ example.name }}.html"
+{% if example['use_svg'] %}
+        style="background-image: url(.{{ image_dir }}/{{ example.name }}-thumb.svg)"
+{% else %}
+        style="background-image: url(.{{ image_dir }}/{{ example.name }}-thumb.png)"
+{% endif %}
+      ></a>
       {% endfor %}
       </div>
     </div>
@@ -130,7 +146,7 @@ def save_example_pngs(examples, image_dir, make_thumbnails=True):
         hashes = {}
 
     for example in examples:
-        filename = example["name"] + ".png"
+        filename = example["name"] + (".svg" if example["use_svg"] else ".png")
         image_file = os.path.join(image_dir, filename)
 
         example_hash = hashlib.md5(example["code"].encode()).hexdigest()
@@ -154,8 +170,13 @@ def save_example_pngs(examples, image_dir, make_thumbnails=True):
 
         if make_thumbnails:
             params = example.get("galleryParameters", {})
-            thumb_file = os.path.join(image_dir, example["name"] + "-thumb.png")
-            create_thumbnail(image_file, thumb_file, **params)
+            if example["use_svg"]:
+                # Thumbnail for SVG is identical to original image
+                thumb_file = os.path.join(image_dir, example["name"] + "-thumb.svg")
+                shutil.copyfile(image_file, thumb_file)
+            else:
+                thumb_file = os.path.join(image_dir, example["name"] + "-thumb.png")
+                create_thumbnail(image_file, thumb_file, **params)
 
     # Save hashes so we know whether we need to re-generate plots
     with open(hash_file, "w") as f:
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index e652d2c3..73e530fc 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -7,12 +7,12 @@ from typing import Any
 
 import jsonschema
 import jsonschema.exceptions
+import jsonschema.validators
 import numpy as np
 import pandas as pd
 
 from altair import vegalite
 
-JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator
 # 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.
@@ -42,7 +42,7 @@ def debug_mode(arg):
         DEBUG_MODE = original
 
 
-def validate_jsonschema(spec, schema, resolver=None):
+def validate_jsonschema(spec, schema, rootschema=None):
     # We don't use jsonschema.validate as this would validate the schema itself.
     # Instead, we pass the schema directly to the validator class. This is done for
     # two reasons: The schema comes from Vega-Lite and is not based on the user
@@ -52,9 +52,18 @@ def validate_jsonschema(spec, schema, resolver=None):
     # e.g. '#/definitions/ValueDefWithCondition<MarkPropFieldOrDatumDef,
     # (Gradient|string|null)>' would be a valid $ref in a Vega-Lite schema but
     # it is not a valid URI reference due to the characters such as '<'.
-    validator = JSONSCHEMA_VALIDATOR(
-        schema, format_checker=JSONSCHEMA_VALIDATOR.FORMAT_CHECKER, resolver=resolver
-    )
+    if rootschema is not None:
+        validator_cls = jsonschema.validators.validator_for(rootschema)
+        resolver = jsonschema.RefResolver.from_schema(rootschema)
+    else:
+        validator_cls = jsonschema.validators.validator_for(schema)
+        # No resolver is necessary if the schema is already the full schema
+        resolver = None
+
+    validator_kwargs = {"resolver": resolver}
+    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
@@ -175,7 +184,6 @@ class SchemaBase(object):
     _schema = None
     _rootschema = None
     _class_is_valid_at_instantiation = True
-    _validator = JSONSCHEMA_VALIDATOR
 
     def __init__(self, *args, **kwds):
         # Two valid options for initialization, which should be handled by
@@ -464,8 +472,9 @@ class SchemaBase(object):
         """
         if schema is None:
             schema = cls._schema
-        resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return validate_jsonschema(instance, schema, resolver=resolver)
+        return validate_jsonschema(
+            instance, schema, rootschema=cls._rootschema or cls._schema
+        )
 
     @classmethod
     def resolve_references(cls, schema=None):
@@ -483,8 +492,9 @@ class SchemaBase(object):
         """
         value = _todict(value, validate=False, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
-        resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return validate_jsonschema(value, props.get(name, {}), resolver=resolver)
+        return validate_jsonschema(
+            value, props.get(name, {}), rootschema=cls._rootschema or cls._schema
+        )
 
     def __dir__(self):
         return list(self._kwds.keys())
@@ -576,9 +586,8 @@ class _FromDict(object):
         if "anyOf" in schema or "oneOf" in schema:
             schemas = schema.get("anyOf", []) + schema.get("oneOf", [])
             for possible_schema in schemas:
-                resolver = jsonschema.RefResolver.from_schema(rootschema)
                 try:
-                    validate_jsonschema(dct, possible_schema, resolver=resolver)
+                    validate_jsonschema(dct, possible_schema, rootschema=rootschema)
                 except jsonschema.ValidationError:
                     continue
                 else:


Test patch:
diff --git a/tests/examples_arguments_syntax/__init__.py b/tests/examples_arguments_syntax/__init__.py
index a9575575..aa8f2abc 100644
--- a/tests/examples_arguments_syntax/__init__.py
+++ b/tests/examples_arguments_syntax/__init__.py
@@ -1,5 +1,9 @@
 import os
 
+# Set of the names of examples that should have SVG static images.
+# This is for examples that VlConvert's PNG export does not support.
+SVG_EXAMPLES = {"isotype_emoji"}
+
 
 def iter_examples_arguments_syntax():
     """Iterate over the examples in this directory.
@@ -7,6 +11,8 @@ def iter_examples_arguments_syntax():
     Each item is a dict with the following keys:
     - "name" : the unique name of the example
     - "filename" : the full file path to the example
+    - "use_svg": Flag indicating whether the static image for the
+        example should be an SVG instead of a PNG
     """
     examples_arguments_syntax_dir = os.path.abspath(os.path.dirname(__file__))
     for filename in os.listdir(examples_arguments_syntax_dir):
@@ -16,4 +22,5 @@ def iter_examples_arguments_syntax():
         yield {
             "name": name,
             "filename": os.path.join(examples_arguments_syntax_dir, filename),
+            "use_svg": name in SVG_EXAMPLES
         }
diff --git a/tests/examples_methods_syntax/__init__.py b/tests/examples_methods_syntax/__init__.py
index 2890d753..5bb277ed 100644
--- a/tests/examples_methods_syntax/__init__.py
+++ b/tests/examples_methods_syntax/__init__.py
@@ -1,5 +1,9 @@
 import os
 
+# Set of the names of examples that should have SVG static images.
+# This is for examples that VlConvert's PNG export does not support.
+SVG_EXAMPLES = {"isotype_emoji"}
+
 
 def iter_examples_methods_syntax():
     """Iterate over the examples in this directory.
@@ -16,4 +20,5 @@ def iter_examples_methods_syntax():
         yield {
             "name": name,
             "filename": os.path.join(examples_methods_syntax_dir, filename),
+            "use_svg": name in SVG_EXAMPLES
         }
diff --git a/tests/utils/tests/test_schemapi.py b/tests/utils/tests/test_schemapi.py
index 00ce019d..b83ac67c 100644
--- a/tests/utils/tests/test_schemapi.py
+++ b/tests/utils/tests/test_schemapi.py
@@ -9,6 +9,7 @@ import pytest
 
 import numpy as np
 
+from altair import load_schema
 from altair.utils.schemapi import (
     UndefinedType,
     SchemaBase,
@@ -17,6 +18,7 @@ from altair.utils.schemapi import (
     SchemaValidationError,
 )
 
+_JSONSCHEMA_DRAFT = load_schema()["$schema"]
 # Make tests inherit from _TestSchema, so that when we test from_dict it won't
 # try to use SchemaBase objects defined elsewhere as wrappers.
 
@@ -29,6 +31,7 @@ class _TestSchema(SchemaBase):
 
 class MySchema(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "definitions": {
             "StringMapping": {
                 "type": "object",
@@ -65,6 +68,7 @@ class StringArray(_TestSchema):
 
 class Derived(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "definitions": {
             "Foo": {"type": "object", "properties": {"d": {"type": "string"}}},
             "Bar": {"type": "string", "enum": ["A", "B"]},
@@ -90,7 +94,10 @@ class Bar(_TestSchema):
 
 
 class SimpleUnion(_TestSchema):
-    _schema = {"anyOf": [{"type": "integer"}, {"type": "string"}]}
+    _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
+        "anyOf": [{"type": "integer"}, {"type": "string"}],
+    }
 
 
 class DefinitionUnion(_TestSchema):
@@ -100,6 +107,7 @@ class DefinitionUnion(_TestSchema):
 
 class SimpleArray(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "type": "array",
         "items": {"anyOf": [{"type": "integer"}, {"type": "string"}]},
     }
@@ -107,11 +115,30 @@ class SimpleArray(_TestSchema):
 
 class InvalidProperties(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "type": "object",
         "properties": {"for": {}, "as": {}, "vega-lite": {}, "$schema": {}},
     }
 
 
+class Draft7Schema(_TestSchema):
+    _schema = {
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "properties": {
+            "e": {"items": [{"type": "string"}, {"type": "string"}]},
+        },
+    }
+
+
+class Draft202012Schema(_TestSchema):
+    _schema = {
+        "$schema": "http://json-schema.org/draft/2020-12/schema#",
+        "properties": {
+            "e": {"items": [{"type": "string"}, {"type": "string"}]},
+        },
+    }
+
+
 def test_construct_multifaceted_schema():
     dct = {
         "a": {"foo": "bar"},
@@ -221,6 +248,21 @@ def test_undefined_singleton():
     assert Undefined is UndefinedType()
 
 
+def test_schema_validator_selection():
+    # Tests if the correct validator class is chosen based on the $schema
+    # property in the schema. Reason for the AttributeError below is, that Draft 2020-12
+    # introduced changes to the "items" keyword, see
+    # https://json-schema.org/draft/2020-12/release-notes.html#changes-to-
+    # items-and-additionalitems
+    dct = {
+        "e": ["a", "b"],
+    }
+
+    assert Draft7Schema.from_dict(dct).to_dict() == dct
+    with pytest.raises(AttributeError, match="'list' object has no attribute 'get'"):
+        Draft202012Schema.from_dict(dct)
+
+
 @pytest.fixture
 def dct():
     return {
diff --git a/tests/vegalite/v5/tests/test_api.py b/tests/vegalite/v5/tests/test_api.py
index 25651424..b2e5aa15 100644
--- a/tests/vegalite/v5/tests/test_api.py
+++ b/tests/vegalite/v5/tests/test_api.py
@@ -335,6 +335,23 @@ def test_save(format, engine, basic_chart):
             os.remove(fp)
 
 
+@pytest.mark.parametrize("inline", [False, True])
+def test_save_html(basic_chart, inline):
+    out = io.StringIO()
+    basic_chart.save(out, format="html", inline=inline)
+    out.seek(0)
+    content = out.read()
+
+    assert content.startswith("<!DOCTYPE html>")
+
+    if inline:
+        assert '<script type="text/javascript">' in content
+    else:
+        assert 'src="https://cdn.jsdelivr.net/npm/vega@5' in content
+        assert 'src="https://cdn.jsdelivr.net/npm/vega-lite@5' in content
+        assert 'src="https://cdn.jsdelivr.net/npm/vega-embed@6' in content
+
+
 def test_facet_basic():
     # wrapped facet
     chart1 = (
@@ -704,13 +721,13 @@ def test_themes():
 
     with alt.themes.enable("default"):
         assert chart.to_dict()["config"] == {
-            "view": {"continuousWidth": 400, "continuousHeight": 300}
+            "view": {"continuousWidth": 300, "continuousHeight": 300}
         }
 
     with alt.themes.enable("opaque"):
         assert chart.to_dict()["config"] == {
             "background": "white",
-            "view": {"continuousWidth": 400, "continuousHeight": 300},
+            "view": {"continuousWidth": 300, "continuousHeight": 300},
         }
 
     with alt.themes.enable("none"):
diff --git a/tests/vegalite/v5/tests/test_layer_props.py b/tests/vegalite/v5/tests/test_layer_props.py
new file mode 100644
index 00000000..1185d504
--- /dev/null
+++ b/tests/vegalite/v5/tests/test_layer_props.py
@@ -0,0 +1,23 @@
+import pytest
+
+import altair.vegalite.v5 as alt
+
+
+def test_layer_props():
+    """Beginning in Vega-Lite v5, the properties "height" and "width" were no longer allowed in a subchart within a LayerChart.  We check here that these are moved to the top level by Altair."""
+    base = alt.Chart().mark_point()
+
+    # Allowed
+    base.properties(width=100) + base
+    base.properties(width=100) + base.properties(height=200)
+    base.properties(width=100) + base.properties(height=200, width=100)
+
+    # Not allowed
+    with pytest.raises(ValueError, match="inconsistent"):
+        base.properties(width=100) + base.properties(width=200)
+
+    # Check that the resulting LayerChart has the correct properties.
+    c = base.properties(width=100) + base.properties(height=200, width=100)
+    assert isinstance(c, alt.LayerChart)
+    assert c.width == 100
+    assert c.height == 200
diff --git a/tests/vegalite/v5/tests/test_theme.py b/tests/vegalite/v5/tests/test_theme.py
index eaa2d889..0eab5546 100644
--- a/tests/vegalite/v5/tests/test_theme.py
+++ b/tests/vegalite/v5/tests/test_theme.py
@@ -15,5 +15,5 @@ def test_vega_themes(chart):
             dct = chart.to_dict()
         assert dct["usermeta"] == {"embedOptions": {"theme": theme}}
         assert dct["config"] == {
-            "view": {"continuousWidth": 400, "continuousHeight": 300}
+            "view": {"continuousWidth": 300, "continuousHeight": 300}
         }
diff --git a/tools/schemapi/tests/test_schemapi.py b/tools/schemapi/tests/test_schemapi.py
index de2ef6fe..40b9b971 100644
--- a/tools/schemapi/tests/test_schemapi.py
+++ b/tools/schemapi/tests/test_schemapi.py
@@ -7,6 +7,7 @@ import pytest
 
 import numpy as np
 
+from altair import load_schema
 from altair.utils.schemapi import (
     UndefinedType,
     SchemaBase,
@@ -15,6 +16,7 @@ from altair.utils.schemapi import (
     SchemaValidationError,
 )
 
+_JSONSCHEMA_DRAFT = load_schema()["$schema"]
 # Make tests inherit from _TestSchema, so that when we test from_dict it won't
 # try to use SchemaBase objects defined elsewhere as wrappers.
 
@@ -27,6 +29,7 @@ class _TestSchema(SchemaBase):
 
 class MySchema(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "definitions": {
             "StringMapping": {
                 "type": "object",
@@ -63,6 +66,7 @@ class StringArray(_TestSchema):
 
 class Derived(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "definitions": {
             "Foo": {"type": "object", "properties": {"d": {"type": "string"}}},
             "Bar": {"type": "string", "enum": ["A", "B"]},
@@ -88,7 +92,10 @@ class Bar(_TestSchema):
 
 
 class SimpleUnion(_TestSchema):
-    _schema = {"anyOf": [{"type": "integer"}, {"type": "string"}]}
+    _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
+        "anyOf": [{"type": "integer"}, {"type": "string"}],
+    }
 
 
 class DefinitionUnion(_TestSchema):
@@ -98,6 +105,7 @@ class DefinitionUnion(_TestSchema):
 
 class SimpleArray(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "type": "array",
         "items": {"anyOf": [{"type": "integer"}, {"type": "string"}]},
     }
@@ -105,11 +113,30 @@ class SimpleArray(_TestSchema):
 
 class InvalidProperties(_TestSchema):
     _schema = {
+        "$schema": _JSONSCHEMA_DRAFT,
         "type": "object",
         "properties": {"for": {}, "as": {}, "vega-lite": {}, "$schema": {}},
     }
 
 
+class Draft7Schema(_TestSchema):
+    _schema = {
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "properties": {
+            "e": {"items": [{"type": "string"}, {"type": "string"}]},
+        },
+    }
+
+
+class Draft202012Schema(_TestSchema):
+    _schema = {
+        "$schema": "http://json-schema.org/draft/2020-12/schema#",
+        "properties": {
+            "e": {"items": [{"type": "string"}, {"type": "string"}]},
+        },
+    }
+
+
 def test_construct_multifaceted_schema():
     dct = {
         "a": {"foo": "bar"},
@@ -219,6 +246,21 @@ def test_undefined_singleton():
     assert Undefined is UndefinedType()
 
 
+def test_schema_validator_selection():
+    # Tests if the correct validator class is chosen based on the $schema
+    # property in the schema. Reason for the AttributeError below is, that Draft 2020-12
+    # introduced changes to the "items" keyword, see
+    # https://json-schema.org/draft/2020-12/release-notes.html#changes-to-
+    # items-and-additionalitems
+    dct = {
+        "e": ["a", "b"],
+    }
+
+    assert Draft7Schema.from_dict(dct).to_dict() == dct
+    with pytest.raises(AttributeError, match="'list' object has no attribute 'get'"):
+        Draft202012Schema.from_dict(dct)
+
+
 @pytest.fixture
 def dct():
     return {