11import dataclasses
2+ import io
23import json
4+ import logging
35from typing import Any
46
57import altair
68import pandas as pd
9+ from edaplot .image_utils import vl_to_png_bytes
710from edaplot .llms import LLMConfig as VegaLLMConfig
811from edaplot .vega import to_altair_chart
912from edaplot .vega_chat .vega_chat import VegaChat , VegaChatConfig
13+ from PIL import Image
1014
1115from databao .configs .llm import LLMConfig
1216from databao .core import ExecutionResult , VisualisationResult , Visualizer
1317from databao .visualizers .vega_vis_tool import VegaVisTool
1418
19+ logger = logging .getLogger (__name__ )
20+
1521
1622class 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
3752def _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