Skip to content

Commit 235eecd

Browse files
authored
Handle invalid Vega-Lite specs that are still drawable (#75)
1 parent 726223e commit 235eecd

File tree

2 files changed

+59
-13
lines changed

2 files changed

+59
-13
lines changed

databao/core/visualizer.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,19 @@ def _repr_mimebundle_(self, include: Any = None, exclude: Any = None) -> Any:
3838
if hasattr(self.plot, "_repr_mimebundle_"):
3939
return self.plot._repr_mimebundle_(include, exclude)
4040

41-
plot_html = self._get_plot_html()
42-
if plot_html is not None:
43-
return {"text/html": plot_html}
41+
mimebundle = {}
42+
if (plot_html := self._get_plot_html()) is not None:
43+
mimebundle["text/html"] = plot_html
44+
45+
# TODO Handle all _repr_*_ methods
46+
# These are mostly for fallback representations
47+
if hasattr(self.plot, "_repr_png_"):
48+
mimebundle["image/png"] = self.plot._repr_png_()
49+
if hasattr(self.plot, "_repr_jpeg_"):
50+
mimebundle["image/jpeg"] = self.plot._repr_jpeg_()
51+
52+
if len(mimebundle) > 0:
53+
return mimebundle
4454
return None
4555

4656
def _get_plot_html(self) -> str | None:

databao/visualizers/vega_chat.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import dataclasses
2+
import io
23
import json
4+
import logging
35
from typing import Any
46

57
import altair
68
import pandas as pd
9+
from edaplot.image_utils import vl_to_png_bytes
710
from edaplot.llms import LLMConfig as VegaLLMConfig
811
from edaplot.vega import to_altair_chart
912
from edaplot.vega_chat.vega_chat import VegaChat, VegaChatConfig
13+
from PIL import Image
1014

1115
from databao.configs.llm import LLMConfig
1216
from databao.core import ExecutionResult, VisualisationResult, Visualizer
1317
from databao.visualizers.vega_vis_tool import VegaVisTool
1418

19+
logger = logging.getLogger(__name__)
20+
1521

1622
class VegaChatResult(VisualisationResult):
23+
plot: VegaVisTool | altair.Chart | Image.Image | None = None
1724
spec: dict[str, Any] | None = None
1825
spec_df: pd.DataFrame | None = None
1926

@@ -33,6 +40,14 @@ def altair(self) -> altair.Chart | None:
3340
return None
3441
return to_altair_chart(self.spec, self.spec_df)
3542

43+
def image(self) -> Image.Image | None:
44+
"""Return a static PIL.Image.Image."""
45+
if self.spec is None or self.spec_df is None:
46+
return None
47+
if (png_bytes := vl_to_png_bytes(self.spec, self.spec_df)) is not None:
48+
return Image.open(io.BytesIO(png_bytes))
49+
return None
50+
3651

3752
def _convert_llm_config(llm_config: LLMConfig) -> VegaLLMConfig:
3853
# N.B. The two config classes are nearly identical.
@@ -74,28 +89,49 @@ def visualize(self, request: str | None, data: ExecutionResult) -> VegaChatResul
7489
model = VegaChat.from_config(config=self._vega_config, df=data.df)
7590
model_out = model.query_sync(request)
7691

92+
# Use the possibly transformed dataframe tied to the generated spec
93+
preprocessed_df = model.dataframe
94+
text = model_out.message.text()
95+
meta = dataclasses.asdict(model_out)
7796
spec = model_out.spec
97+
spec_json = json.dumps(spec, indent=2) if spec is not None else None
7898
if spec is None or not model_out.is_drawable or model_out.is_empty_chart:
7999
return VegaChatResult(
80-
text=f"Failed to visualize request {request}",
81-
meta=dataclasses.asdict(model_out),
100+
text=f"Failed to visualize request! Output: {text}",
101+
meta=meta,
82102
plot=None,
83-
code=None,
103+
code=spec_json,
104+
spec=spec,
105+
spec_df=preprocessed_df,
84106
)
85107

86-
text = model_out.message.text()
87-
spec_json = json.dumps(spec, indent=2)
88-
89-
# Use the possibly transformed dataframe tied to the generated spec
90-
preprocessed_df = model.dataframe
91-
if self._return_interactive_chart:
108+
if not model_out.is_valid_schema and model_out.is_drawable:
109+
# Vega-Lite specs can be invalid (so cannot be used with altair), but they might still be drawable with
110+
# another backend.
111+
logger.warning("Generated Vega-Lite spec is not valid, but it is still drawable: %s", spec_json)
112+
if self._return_interactive_chart:
113+
# The VegaVisTool backend uses vega-embed so it can handle corrupt specs
114+
plot = VegaVisTool(spec, preprocessed_df)
115+
elif (png_bytes := vl_to_png_bytes(spec, preprocessed_df)) is not None:
116+
# Try to convert to an Image that can still be displayed in Jupyter notebooks
117+
plot = Image.open(io.BytesIO(png_bytes))
118+
else:
119+
return VegaChatResult(
120+
text=f"Failed to visualize request! Output: {text}",
121+
meta=meta,
122+
plot=None,
123+
code=spec_json,
124+
spec=spec,
125+
spec_df=preprocessed_df,
126+
)
127+
elif self._return_interactive_chart:
92128
plot = VegaVisTool(spec, preprocessed_df)
93129
else:
94130
plot = to_altair_chart(spec, preprocessed_df)
95131

96132
return VegaChatResult(
97133
text=text,
98-
meta=dataclasses.asdict(model_out),
134+
meta=meta,
99135
plot=plot,
100136
code=spec_json,
101137
spec=spec,

0 commit comments

Comments
 (0)