Skip to content

Commit 386cba6

Browse files
authored
fix: handle list-type tooltip encoding in altair chart (#9167) (#9175)
`_has_binning()` and `_get_binned_fields()` iterated over `spec["encoding"].values()` and called dict methods (`.get()`, `in`) on each value. When `tooltip` is defined as a list of multiple tooltips, the encoding value is a list instead of a dict, causing `AttributeError: 'list' object has no attribute 'get'`. Skip list-type encoding values (like tooltip arrays) since they cannot contain binning configuration. Closes #9167
1 parent d616b3c commit 386cba6

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

marimo/_plugins/ui/_impl/altair_chart.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def _has_binning(spec: VegaSpec) -> bool:
6969
if "encoding" not in spec:
7070
return False
7171
for encoding in spec["encoding"].values():
72+
if isinstance(encoding, list):
73+
continue
7274
if "bin" in encoding:
7375
return True
7476
return False
@@ -85,6 +87,8 @@ def _get_binned_fields(spec: VegaSpec) -> dict[str, Any]:
8587
return binned_fields
8688

8789
for encoding in spec["encoding"].values():
90+
if isinstance(encoding, list):
91+
continue
8892
if encoding.get("bin"):
8993
# Get the field name
9094
field = encoding.get("field")

tests/_plugins/ui/_impl/test_altair_chart.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,62 @@ def test_get_binned_fields() -> None:
993993
assert binned_fields["values"]["extent"] == [0, 50]
994994

995995

996+
@pytest.mark.skipif(not HAS_DEPS, reason="optional dependencies not installed")
997+
def test_get_binned_fields_with_tooltip_list() -> None:
998+
"""Test _get_binned_fields handles tooltip encoded as a list (#9167)."""
999+
import altair as alt
1000+
1001+
# Tooltip as a list should not crash _get_binned_fields or _has_binning
1002+
spec = _parse_spec(
1003+
alt.Chart(pd.DataFrame({"x": range(10), "y": range(10)}))
1004+
.mark_point()
1005+
.encode(
1006+
x="x",
1007+
y="y",
1008+
tooltip=["x", "y"],
1009+
)
1010+
)
1011+
assert _get_binned_fields(spec) == {}
1012+
assert _has_binning(spec) is False
1013+
1014+
# Tooltip list with binned fields on other channels
1015+
spec_with_bins = _parse_spec(
1016+
alt.Chart(pd.DataFrame({"x": range(10), "y": range(10)}))
1017+
.mark_bar()
1018+
.encode(
1019+
x=alt.X("x", bin=True),
1020+
y="count()",
1021+
tooltip=["x", alt.Tooltip("y", format=".2f")],
1022+
)
1023+
)
1024+
binned = _get_binned_fields(spec_with_bins)
1025+
assert "x" in binned
1026+
assert _has_binning(spec_with_bins) is True
1027+
1028+
1029+
@pytest.mark.skipif(not HAS_DEPS, reason="optional dependencies not installed")
1030+
def test_altair_chart_with_tooltip_list() -> None:
1031+
"""Smoke test: altair_chart with tooltip list should not error (#9167)."""
1032+
import altair as alt
1033+
1034+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": ["x", "y", "z"]})
1035+
chart = (
1036+
alt.Chart(df)
1037+
.mark_arc()
1038+
.encode(
1039+
theta="a",
1040+
color="c",
1041+
tooltip=[
1042+
"c",
1043+
alt.Tooltip("a", format=".2f"),
1044+
alt.Tooltip("b", format="$.2f"),
1045+
],
1046+
)
1047+
)
1048+
# This should not raise AttributeError
1049+
altair_chart(chart)
1050+
1051+
9961052
@pytest.mark.skipif(not HAS_DEPS, reason="optional dependencies not installed")
9971053
def test_has_geoshape() -> None:
9981054
import altair as alt

0 commit comments

Comments
 (0)