Skip to content

Commit ccbe5bb

Browse files
cpsievertclaude
andcommitted
feat(python): add VegaLiteWriter.render_chart() for Altair output
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b602c39 commit ccbe5bb

2 files changed

Lines changed: 99 additions & 19 deletions

File tree

ggsql-python/python/ggsql/__init__.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ggsql._ggsql import (
1111
DuckDBReader,
12-
VegaLiteWriter,
12+
VegaLiteWriter as _RustVegaLiteWriter,
1313
Validated,
1414
Spec,
1515
validate,
@@ -41,6 +41,64 @@
4141
]
4242

4343

44+
def _json_to_altair_chart(vegalite_json: str, **kwargs: Any) -> AltairChart:
45+
"""Convert a Vega-Lite JSON string to the appropriate Altair chart type."""
46+
spec = json.loads(vegalite_json)
47+
48+
if "layer" in spec:
49+
return altair.LayerChart.from_json(vegalite_json, **kwargs)
50+
elif "facet" in spec or "spec" in spec:
51+
return altair.FacetChart.from_json(vegalite_json, **kwargs)
52+
elif "concat" in spec:
53+
return altair.ConcatChart.from_json(vegalite_json, **kwargs)
54+
elif "hconcat" in spec:
55+
return altair.HConcatChart.from_json(vegalite_json, **kwargs)
56+
elif "vconcat" in spec:
57+
return altair.VConcatChart.from_json(vegalite_json, **kwargs)
58+
elif "repeat" in spec:
59+
return altair.RepeatChart.from_json(vegalite_json, **kwargs)
60+
else:
61+
return altair.Chart.from_json(vegalite_json, **kwargs)
62+
63+
64+
class VegaLiteWriter:
65+
"""Vega-Lite v6 JSON output writer.
66+
67+
Methods
68+
-------
69+
render(spec)
70+
Render a Spec to a Vega-Lite JSON string.
71+
render_chart(spec, **kwargs)
72+
Render a Spec to an Altair chart object.
73+
"""
74+
75+
def __init__(self) -> None:
76+
self._inner = _RustVegaLiteWriter()
77+
78+
def render(self, spec: Spec) -> str:
79+
"""Render a Spec to a Vega-Lite JSON string."""
80+
return self._inner.render(spec)
81+
82+
def render_chart(self, spec: Spec, **kwargs: Any) -> AltairChart:
83+
"""Render a Spec to an Altair chart object.
84+
85+
Parameters
86+
----------
87+
spec
88+
The resolved visualization specification from ``reader.execute()``.
89+
**kwargs
90+
Additional keyword arguments passed to ``altair.Chart.from_json()``.
91+
Common options include ``validate=False`` to skip schema validation.
92+
93+
Returns
94+
-------
95+
AltairChart
96+
An Altair chart object (Chart, LayerChart, FacetChart, etc.).
97+
"""
98+
vegalite_json = self.render(spec)
99+
return _json_to_altair_chart(vegalite_json, **kwargs)
100+
101+
44102
def render_altair(
45103
df: IntoFrame,
46104
viz: str,
@@ -86,21 +144,4 @@ def render_altair(
86144
writer = VegaLiteWriter()
87145
vegalite_json = writer.render(spec)
88146

89-
# Parse to determine the correct Altair class
90-
spec = json.loads(vegalite_json)
91-
92-
# Determine the correct Altair class based on spec structure
93-
if "layer" in spec:
94-
return altair.LayerChart.from_json(vegalite_json, **kwargs)
95-
elif "facet" in spec or "spec" in spec:
96-
return altair.FacetChart.from_json(vegalite_json, **kwargs)
97-
elif "concat" in spec:
98-
return altair.ConcatChart.from_json(vegalite_json, **kwargs)
99-
elif "hconcat" in spec:
100-
return altair.HConcatChart.from_json(vegalite_json, **kwargs)
101-
elif "vconcat" in spec:
102-
return altair.VConcatChart.from_json(vegalite_json, **kwargs)
103-
elif "repeat" in spec:
104-
return altair.RepeatChart.from_json(vegalite_json, **kwargs)
105-
else:
106-
return altair.Chart.from_json(vegalite_json, **kwargs)
147+
return _json_to_altair_chart(vegalite_json, **kwargs)

ggsql-python/tests/test_ggsql.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,42 @@ def unregister(self, name: str) -> None:
530530
writer = ggsql.VegaLiteWriter()
531531
json_output = writer.render(spec)
532532
assert "point" in json_output
533+
534+
535+
class TestVegaLiteWriterRenderChart:
536+
"""Tests for VegaLiteWriter.render_chart() method."""
537+
538+
def test_render_chart_returns_altair_chart(self):
539+
"""render_chart() returns an Altair chart object."""
540+
reader = ggsql.DuckDBReader("duckdb://memory")
541+
spec = reader.execute("SELECT 1 AS x, 2 AS y VISUALISE x, y DRAW point")
542+
writer = ggsql.VegaLiteWriter()
543+
chart = writer.render_chart(spec)
544+
assert isinstance(chart, altair.TopLevelMixin)
545+
546+
def test_render_chart_layer(self):
547+
"""render_chart() returns LayerChart for layered specs."""
548+
reader = ggsql.DuckDBReader("duckdb://memory")
549+
spec = reader.execute("SELECT 1 AS x, 2 AS y VISUALISE x, y DRAW point")
550+
writer = ggsql.VegaLiteWriter()
551+
chart = writer.render_chart(spec)
552+
assert isinstance(chart, altair.LayerChart)
553+
554+
def test_render_chart_facet(self):
555+
"""render_chart() returns FacetChart for faceted specs."""
556+
reader = ggsql.DuckDBReader("duckdb://memory")
557+
df = pl.DataFrame(
558+
{
559+
"x": [1, 2, 3, 4, 5, 6],
560+
"y": [10, 20, 30, 40, 50, 60],
561+
"group": ["A", "A", "A", "B", "B", "B"],
562+
}
563+
)
564+
reader.register("data", df)
565+
spec = reader.execute(
566+
"SELECT * FROM data VISUALISE x, y FACET group DRAW point"
567+
)
568+
writer = ggsql.VegaLiteWriter()
569+
chart = writer.render_chart(spec, validate=False)
570+
assert isinstance(chart, altair.FacetChart)
571+

0 commit comments

Comments
 (0)