From 870bcd98b2da6a9194c757b97cde4bc4174d4662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Bobot?= Date: Mon, 13 Oct 2025 13:38:10 +0200 Subject: [PATCH 1/5] Repair vega export Even if not advertise, it is better to not fail for vega by going in the svg case --- altair/utils/save.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/altair/utils/save.py b/altair/utils/save.py index a39387fe8..062a66047 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -200,6 +200,9 @@ def perform_save() -> None: ) if format == "pdf": write_file_or_filename(fp, mb_any["application/pdf"], mode="wb") + elif format == "vega": + json_spec = json.dumps(mb_any["application/vnd.vega.v5+json"], **json_kwds) + write_file_or_filename(fp, json_spec, mode="w", encoding=encoding) else: write_file_or_filename( fp, mb_any["image/svg+xml"], mode="w", encoding=encoding From 2e0a01ac9b7c74d5a7d8fabad74c19366cf12907 Mon Sep 17 00:00:00 2001 From: Mattijn van Hoek Date: Thu, 6 Nov 2025 17:39:11 +0100 Subject: [PATCH 2/5] improve fix and add test --- altair/utils/save.py | 10 +++++----- tests/vegalite/v6/test_api.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 062a66047..15eb198f5 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -80,7 +80,7 @@ def save( fp: str | Path | IO, vega_version: str | None, vegaembed_version: str | None, - format: Literal["json", "html", "png", "svg", "pdf"] | None = None, + format: Literal["json", "html", "png", "svg", "pdf", "vega"] | None = None, mode: Literal["vega-lite"] | None = None, vegalite_version: str | None = None, embed_options: dict | None = None, @@ -93,7 +93,7 @@ def save( """ Save a chart to file in a variety of formats. - Supported formats are [json, html, png, svg, pdf] + Supported formats are [json, html, png, svg, pdf, vega] Parameters ---------- @@ -102,7 +102,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', 'pdf']. + the format to write: one of ['json', 'html', 'png', 'svg', 'pdf', 'vega']. If not specified, the format will be determined from the filename. mode : string (optional) Must be 'vega-lite'. If not specified, then infer the mode from @@ -123,7 +123,7 @@ def save( scale_factor : float (optional) scale_factor to use to change size/resolution of png or svg output engine: string {'vl-convert'} - the conversion engine to use for 'png', 'svg', and 'pdf' formats + the conversion engine to use for 'png', 'svg', 'pdf', and 'vega' formats inline: bool (optional) If False (default), the required JavaScript libraries are loaded from a CDN location in the resulting html file. @@ -201,7 +201,7 @@ def perform_save() -> None: if format == "pdf": write_file_or_filename(fp, mb_any["application/pdf"], mode="wb") elif format == "vega": - json_spec = json.dumps(mb_any["application/vnd.vega.v5+json"], **json_kwds) + json_spec = json.dumps(mb_any["application/vnd.vega.v6+json"], **json_kwds) write_file_or_filename(fp, json_spec, mode="w", encoding=encoding) else: write_file_or_filename( diff --git a/tests/vegalite/v6/test_api.py b/tests/vegalite/v6/test_api.py index bf7997afd..3b4b5309a 100644 --- a/tests/vegalite/v6/test_api.py +++ b/tests/vegalite/v6/test_api.py @@ -819,7 +819,7 @@ def test_selection_expression(): getattr(selection, magic_attr) -@pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf", "bogus"]) +@pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf", "vega", "bogus"]) @pytest.mark.parametrize("engine", ["vl-convert"]) def test_save(format, engine, basic_chart): if format in {"pdf", "png"}: @@ -829,7 +829,7 @@ def test_save(format, engine, basic_chart): out = io.StringIO() mode = "r" - if format in {"svg", "png", "pdf", "bogus"} and engine == "vl-convert": + if format in {"svg", "png", "pdf", "vega", "bogus"} and engine == "vl-convert": if format == "bogus": with pytest.raises(ValueError) as err: # noqa: PT011 basic_chart.save(out, format=format, engine=engine) @@ -847,6 +847,15 @@ def test_save(format, engine, basic_chart): if format == "json": assert "$schema" in json.loads(content) + elif format == "vega": + vega_spec = json.loads(content) + assert "$schema" in vega_spec + # Verify it's a Vega schema, not Vega-Lite + assert "vega/v" in vega_spec["$schema"] + assert "vega-lite" not in vega_spec["$schema"] + # Verify it has Vega-specific structures + assert "marks" in vega_spec + assert "scales" in vega_spec elif format == "html": assert isinstance(content, str) assert content.startswith("") From 529d22e043e784140e742242cda0ae8ea3e81d67 Mon Sep 17 00:00:00 2001 From: Mattijn van Hoek Date: Thu, 6 Nov 2025 17:52:19 +0100 Subject: [PATCH 3/5] ruff changes --- tests/vegalite/v6/test_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/vegalite/v6/test_api.py b/tests/vegalite/v6/test_api.py index 5798e3e6c..340b762a5 100644 --- a/tests/vegalite/v6/test_api.py +++ b/tests/vegalite/v6/test_api.py @@ -825,7 +825,9 @@ def test_selection_expression(): getattr(selection, magic_attr) -@pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf", "vega", "bogus"]) +@pytest.mark.parametrize( + "format", ["html", "json", "png", "svg", "pdf", "vega", "bogus"] +) @pytest.mark.parametrize("engine", ["vl-convert"]) def test_save(format, engine, basic_chart): if format in {"pdf", "png"}: From d1b5a7e7f30be38ebae7b51e5286061f8f931787 Mon Sep 17 00:00:00 2001 From: Mattijn van Hoek Date: Thu, 6 Nov 2025 22:35:13 +0100 Subject: [PATCH 4/5] fix ruff, reduce complexity --- altair/utils/save.py | 123 ++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 42 deletions(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 15eb198f5..3c074ea1b 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -75,6 +75,81 @@ def set_inspect_mode_argument( return mode +def _save_mimebundle_format( + spec: dict[str, Any], + format: Literal["html", "png", "svg", "pdf", "vega"], + fp: str | Path | IO, + inner_mode: Literal["vega-lite"], + vega_version: str | None, + vegalite_version: str | None, + vegaembed_version: str | None, + embed_options: dict[str, Any] | None, + json_kwds: dict[str, Any], + encoding: str, + scale_factor: float, + engine: Literal["vl-convert"] | None, + inline: bool, + **kwargs: Any, +) -> None: + """Save chart using spec_to_mimebundle for formats that require it.""" + if format == "html" and inline: + kwargs["template"] = "inline" + + common_kwargs = { + "spec": spec, + "format": format, + "mode": inner_mode, + "vega_version": vega_version, + "vegalite_version": vegalite_version, + "vegaembed_version": vegaembed_version, + "embed_options": embed_options, + } + + if format == "html": + common_kwargs["json_kwds"] = json_kwds + mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) + write_file_or_filename(fp, mb_result["text/html"], mode="w", encoding=encoding) + elif format == "png": + common_kwargs.update( + { + "scale_factor": scale_factor, + "engine": engine, + } + ) + mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) + write_file_or_filename(fp, mb_result[0]["image/png"], mode="wb") + elif format == "svg": + common_kwargs.update( + { + "scale_factor": scale_factor, + "engine": engine, + } + ) + mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) + write_file_or_filename( + fp, mb_result["image/svg+xml"], mode="w", encoding=encoding + ) + elif format == "pdf": + common_kwargs.update( + { + "scale_factor": scale_factor, + "engine": engine, + } + ) + mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) + write_file_or_filename(fp, mb_result["application/pdf"], mode="wb") + else: # vega + common_kwargs.update( + { + "scale_factor": scale_factor, + "engine": engine, + } + ) + mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) + json_spec = json.dumps(mb_result["application/vnd.vega.v6+json"], **json_kwds) + write_file_or_filename(fp, json_spec, mode="w", encoding=encoding) + + def save( chart, fp: str | Path | IO, @@ -154,59 +229,23 @@ def perform_save() -> None: if format == "json": json_spec = json.dumps(spec, **json_kwds) write_file_or_filename(fp, json_spec, mode="w", encoding=encoding) - elif format == "html": - if inline: - kwargs["template"] = "inline" - mb_html = spec_to_mimebundle( + elif format in {"html", "png", "svg", "pdf", "vega"}: + _save_mimebundle_format( spec=spec, format=format, - mode=inner_mode, + fp=fp, + inner_mode=inner_mode, vega_version=vega_version, vegalite_version=vegalite_version, vegaembed_version=vegaembed_version, embed_options=embed_options, json_kwds=json_kwds, - **kwargs, - ) - write_file_or_filename( - fp, mb_html["text/html"], mode="w", encoding=encoding - ) - elif format == "png": - mb_png = spec_to_mimebundle( - spec=spec, - format=format, - mode=inner_mode, - vega_version=vega_version, - vegalite_version=vegalite_version, - vegaembed_version=vegaembed_version, - embed_options=embed_options, - scale_factor=scale_factor, - engine=engine, - **kwargs, - ) - write_file_or_filename(fp, mb_png[0]["image/png"], mode="wb") - elif format in {"svg", "pdf", "vega"}: - mb_any = spec_to_mimebundle( - spec=spec, - format=format, - mode=inner_mode, - vega_version=vega_version, - vegalite_version=vegalite_version, - vegaembed_version=vegaembed_version, - embed_options=embed_options, + encoding=encoding, scale_factor=scale_factor, engine=engine, + inline=inline, **kwargs, ) - if format == "pdf": - write_file_or_filename(fp, mb_any["application/pdf"], mode="wb") - elif format == "vega": - json_spec = json.dumps(mb_any["application/vnd.vega.v6+json"], **json_kwds) - write_file_or_filename(fp, json_spec, mode="w", encoding=encoding) - else: - write_file_or_filename( - fp, mb_any["image/svg+xml"], mode="w", encoding=encoding - ) else: msg = f"Unsupported format: '{format}'" raise ValueError(msg) From 02aa0f724fde36a4e018673784a1b2eb35e0fb0a Mon Sep 17 00:00:00 2001 From: Mattijn van Hoek Date: Thu, 6 Nov 2025 22:40:43 +0100 Subject: [PATCH 5/5] fix mypy --- altair/utils/save.py | 98 ++++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/altair/utils/save.py b/altair/utils/save.py index 3c074ea1b..da8141c5e 100644 --- a/altair/utils/save.py +++ b/altair/utils/save.py @@ -92,60 +92,78 @@ def _save_mimebundle_format( **kwargs: Any, ) -> None: """Save chart using spec_to_mimebundle for formats that require it.""" - if format == "html" and inline: - kwargs["template"] = "inline" - - common_kwargs = { - "spec": spec, - "format": format, - "mode": inner_mode, - "vega_version": vega_version, - "vegalite_version": vegalite_version, - "vegaembed_version": vegaembed_version, - "embed_options": embed_options, - } - if format == "html": - common_kwargs["json_kwds"] = json_kwds - mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) + if inline: + kwargs["template"] = "inline" + mb_result: dict[str, str] = spec_to_mimebundle( + spec=spec, + format=format, + mode=inner_mode, + vega_version=vega_version, + vegalite_version=vegalite_version, + vegaembed_version=vegaembed_version, + embed_options=embed_options, + json_kwds=json_kwds, + **kwargs, + ) write_file_or_filename(fp, mb_result["text/html"], mode="w", encoding=encoding) elif format == "png": - common_kwargs.update( - { - "scale_factor": scale_factor, - "engine": engine, - } + mb_result_png: tuple[dict[str, Any], dict[str, Any]] = spec_to_mimebundle( + spec=spec, + format=format, + mode=inner_mode, + vega_version=vega_version, + vegalite_version=vegalite_version, + vegaembed_version=vegaembed_version, + embed_options=embed_options, + scale_factor=scale_factor, + engine=engine, + **kwargs, ) - mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) - write_file_or_filename(fp, mb_result[0]["image/png"], mode="wb") + write_file_or_filename(fp, mb_result_png[0]["image/png"], mode="wb") elif format == "svg": - common_kwargs.update( - { - "scale_factor": scale_factor, - "engine": engine, - } + mb_result = spec_to_mimebundle( + spec=spec, + format=format, + mode=inner_mode, + vega_version=vega_version, + vegalite_version=vegalite_version, + vegaembed_version=vegaembed_version, + embed_options=embed_options, + scale_factor=scale_factor, + engine=engine, + **kwargs, ) - mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) write_file_or_filename( fp, mb_result["image/svg+xml"], mode="w", encoding=encoding ) elif format == "pdf": - common_kwargs.update( - { - "scale_factor": scale_factor, - "engine": engine, - } + mb_result = spec_to_mimebundle( + spec=spec, + format=format, + mode=inner_mode, + vega_version=vega_version, + vegalite_version=vegalite_version, + vegaembed_version=vegaembed_version, + embed_options=embed_options, + scale_factor=scale_factor, + engine=engine, + **kwargs, ) - mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) write_file_or_filename(fp, mb_result["application/pdf"], mode="wb") else: # vega - common_kwargs.update( - { - "scale_factor": scale_factor, - "engine": engine, - } + mb_result = spec_to_mimebundle( + spec=spec, + format=format, + mode=inner_mode, + vega_version=vega_version, + vegalite_version=vegalite_version, + vegaembed_version=vegaembed_version, + embed_options=embed_options, + scale_factor=scale_factor, + engine=engine, + **kwargs, ) - mb_result = spec_to_mimebundle(**common_kwargs, **kwargs) json_spec = json.dumps(mb_result["application/vnd.vega.v6+json"], **json_kwds) write_file_or_filename(fp, json_spec, mode="w", encoding=encoding)