Skip to content

Commit 6ff27ce

Browse files
bobotmattijn
andauthored
fix: Repair vega export (#3889)
* Repair vega export Even if not advertise, it is better to not fail for vega by going in the svg case * improve fix and add test * ruff changes * fix ruff, reduce complexity * fix mypy --------- Co-authored-by: Mattijn van Hoek <[email protected]>
1 parent 6fd632b commit 6ff27ce

File tree

2 files changed

+116
-45
lines changed

2 files changed

+116
-45
lines changed

altair/utils/save.py

Lines changed: 103 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,105 @@ def set_inspect_mode_argument(
7575
return mode
7676

7777

78+
def _save_mimebundle_format(
79+
spec: dict[str, Any],
80+
format: Literal["html", "png", "svg", "pdf", "vega"],
81+
fp: str | Path | IO,
82+
inner_mode: Literal["vega-lite"],
83+
vega_version: str | None,
84+
vegalite_version: str | None,
85+
vegaembed_version: str | None,
86+
embed_options: dict[str, Any] | None,
87+
json_kwds: dict[str, Any],
88+
encoding: str,
89+
scale_factor: float,
90+
engine: Literal["vl-convert"] | None,
91+
inline: bool,
92+
**kwargs: Any,
93+
) -> None:
94+
"""Save chart using spec_to_mimebundle for formats that require it."""
95+
if format == "html":
96+
if inline:
97+
kwargs["template"] = "inline"
98+
mb_result: dict[str, str] = spec_to_mimebundle(
99+
spec=spec,
100+
format=format,
101+
mode=inner_mode,
102+
vega_version=vega_version,
103+
vegalite_version=vegalite_version,
104+
vegaembed_version=vegaembed_version,
105+
embed_options=embed_options,
106+
json_kwds=json_kwds,
107+
**kwargs,
108+
)
109+
write_file_or_filename(fp, mb_result["text/html"], mode="w", encoding=encoding)
110+
elif format == "png":
111+
mb_result_png: tuple[dict[str, Any], dict[str, Any]] = spec_to_mimebundle(
112+
spec=spec,
113+
format=format,
114+
mode=inner_mode,
115+
vega_version=vega_version,
116+
vegalite_version=vegalite_version,
117+
vegaembed_version=vegaembed_version,
118+
embed_options=embed_options,
119+
scale_factor=scale_factor,
120+
engine=engine,
121+
**kwargs,
122+
)
123+
write_file_or_filename(fp, mb_result_png[0]["image/png"], mode="wb")
124+
elif format == "svg":
125+
mb_result = spec_to_mimebundle(
126+
spec=spec,
127+
format=format,
128+
mode=inner_mode,
129+
vega_version=vega_version,
130+
vegalite_version=vegalite_version,
131+
vegaembed_version=vegaembed_version,
132+
embed_options=embed_options,
133+
scale_factor=scale_factor,
134+
engine=engine,
135+
**kwargs,
136+
)
137+
write_file_or_filename(
138+
fp, mb_result["image/svg+xml"], mode="w", encoding=encoding
139+
)
140+
elif format == "pdf":
141+
mb_result = spec_to_mimebundle(
142+
spec=spec,
143+
format=format,
144+
mode=inner_mode,
145+
vega_version=vega_version,
146+
vegalite_version=vegalite_version,
147+
vegaembed_version=vegaembed_version,
148+
embed_options=embed_options,
149+
scale_factor=scale_factor,
150+
engine=engine,
151+
**kwargs,
152+
)
153+
write_file_or_filename(fp, mb_result["application/pdf"], mode="wb")
154+
else: # vega
155+
mb_result = spec_to_mimebundle(
156+
spec=spec,
157+
format=format,
158+
mode=inner_mode,
159+
vega_version=vega_version,
160+
vegalite_version=vegalite_version,
161+
vegaembed_version=vegaembed_version,
162+
embed_options=embed_options,
163+
scale_factor=scale_factor,
164+
engine=engine,
165+
**kwargs,
166+
)
167+
json_spec = json.dumps(mb_result["application/vnd.vega.v6+json"], **json_kwds)
168+
write_file_or_filename(fp, json_spec, mode="w", encoding=encoding)
169+
170+
78171
def save(
79172
chart,
80173
fp: str | Path | IO,
81174
vega_version: str | None,
82175
vegaembed_version: str | None,
83-
format: Literal["json", "html", "png", "svg", "pdf"] | None = None,
176+
format: Literal["json", "html", "png", "svg", "pdf", "vega"] | None = None,
84177
mode: Literal["vega-lite"] | None = None,
85178
vegalite_version: str | None = None,
86179
embed_options: dict | None = None,
@@ -93,7 +186,7 @@ def save(
93186
"""
94187
Save a chart to file in a variety of formats.
95188
96-
Supported formats are [json, html, png, svg, pdf]
189+
Supported formats are [json, html, png, svg, pdf, vega]
97190
98191
Parameters
99192
----------
@@ -102,7 +195,7 @@ def save(
102195
fp : string filename, pathlib.Path or file-like object
103196
file to which to write the chart.
104197
format : string (optional)
105-
the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
198+
the format to write: one of ['json', 'html', 'png', 'svg', 'pdf', 'vega'].
106199
If not specified, the format will be determined from the filename.
107200
mode : string (optional)
108201
Must be 'vega-lite'. If not specified, then infer the mode from
@@ -123,7 +216,7 @@ def save(
123216
scale_factor : float (optional)
124217
scale_factor to use to change size/resolution of png or svg output
125218
engine: string {'vl-convert'}
126-
the conversion engine to use for 'png', 'svg', and 'pdf' formats
219+
the conversion engine to use for 'png', 'svg', 'pdf', and 'vega' formats
127220
inline: bool (optional)
128221
If False (default), the required JavaScript libraries are loaded
129222
from a CDN location in the resulting html file.
@@ -154,56 +247,23 @@ def perform_save() -> None:
154247
if format == "json":
155248
json_spec = json.dumps(spec, **json_kwds)
156249
write_file_or_filename(fp, json_spec, mode="w", encoding=encoding)
157-
elif format == "html":
158-
if inline:
159-
kwargs["template"] = "inline"
160-
mb_html = spec_to_mimebundle(
250+
elif format in {"html", "png", "svg", "pdf", "vega"}:
251+
_save_mimebundle_format(
161252
spec=spec,
162253
format=format,
163-
mode=inner_mode,
254+
fp=fp,
255+
inner_mode=inner_mode,
164256
vega_version=vega_version,
165257
vegalite_version=vegalite_version,
166258
vegaembed_version=vegaembed_version,
167259
embed_options=embed_options,
168260
json_kwds=json_kwds,
169-
**kwargs,
170-
)
171-
write_file_or_filename(
172-
fp, mb_html["text/html"], mode="w", encoding=encoding
173-
)
174-
elif format == "png":
175-
mb_png = spec_to_mimebundle(
176-
spec=spec,
177-
format=format,
178-
mode=inner_mode,
179-
vega_version=vega_version,
180-
vegalite_version=vegalite_version,
181-
vegaembed_version=vegaembed_version,
182-
embed_options=embed_options,
183-
scale_factor=scale_factor,
184-
engine=engine,
185-
**kwargs,
186-
)
187-
write_file_or_filename(fp, mb_png[0]["image/png"], mode="wb")
188-
elif format in {"svg", "pdf", "vega"}:
189-
mb_any = spec_to_mimebundle(
190-
spec=spec,
191-
format=format,
192-
mode=inner_mode,
193-
vega_version=vega_version,
194-
vegalite_version=vegalite_version,
195-
vegaembed_version=vegaembed_version,
196-
embed_options=embed_options,
261+
encoding=encoding,
197262
scale_factor=scale_factor,
198263
engine=engine,
264+
inline=inline,
199265
**kwargs,
200266
)
201-
if format == "pdf":
202-
write_file_or_filename(fp, mb_any["application/pdf"], mode="wb")
203-
else:
204-
write_file_or_filename(
205-
fp, mb_any["image/svg+xml"], mode="w", encoding=encoding
206-
)
207267
else:
208268
msg = f"Unsupported format: '{format}'"
209269
raise ValueError(msg)

tests/vegalite/v6/test_api.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,9 @@ def test_selection_expression():
825825
getattr(selection, magic_attr)
826826

827827

828-
@pytest.mark.parametrize("format", ["html", "json", "png", "svg", "pdf", "bogus"])
828+
@pytest.mark.parametrize(
829+
"format", ["html", "json", "png", "svg", "pdf", "vega", "bogus"]
830+
)
829831
@pytest.mark.parametrize("engine", ["vl-convert"])
830832
def test_save(format, engine, basic_chart):
831833
if format in {"pdf", "png"}:
@@ -835,7 +837,7 @@ def test_save(format, engine, basic_chart):
835837
out = io.StringIO()
836838
mode = "r"
837839

838-
if format in {"svg", "png", "pdf", "bogus"} and engine == "vl-convert":
840+
if format in {"svg", "png", "pdf", "vega", "bogus"} and engine == "vl-convert":
839841
if format == "bogus":
840842
with pytest.raises(ValueError) as err: # noqa: PT011
841843
basic_chart.save(out, format=format, engine=engine)
@@ -853,6 +855,15 @@ def test_save(format, engine, basic_chart):
853855

854856
if format == "json":
855857
assert "$schema" in json.loads(content)
858+
elif format == "vega":
859+
vega_spec = json.loads(content)
860+
assert "$schema" in vega_spec
861+
# Verify it's a Vega schema, not Vega-Lite
862+
assert "vega/v" in vega_spec["$schema"]
863+
assert "vega-lite" not in vega_spec["$schema"]
864+
# Verify it has Vega-specific structures
865+
assert "marks" in vega_spec
866+
assert "scales" in vega_spec
856867
elif format == "html":
857868
assert isinstance(content, str)
858869
assert content.startswith("<!DOCTYPE html>")

0 commit comments

Comments
 (0)