Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion altair/expr/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def __init__(self, group, name) -> None:
super().__init__(group=group, name=name)

def __repr__(self) -> str:
return f"{self.group}[{self.name!r}]"
return f"{self.group}[{_js_repr(self.name)}]"


IntoExpression: TypeAlias = Union[
Expand Down
14 changes: 12 additions & 2 deletions altair/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,8 +895,9 @@ def get_encoding(self, tp: type[Any], /) -> str:
msg = f"positional of type {type(tp).__name__!r}"
raise NotImplementedError(msg)

def _wrap_in_channel(self, obj: Any, encoding: str, /):
from altair.expr.core import Expression
def _wrap_in_channel(self, obj: Any, encoding: str, /): # noqa: C901
from altair.expr.core import Expression, GetItemExpression
from altair.vegalite.v6.api import Parameter
from altair.vegalite.v6.schema.core import ExprRef

if isinstance(obj, (Expression, ExprRef)):
Expand All @@ -907,6 +908,15 @@ def _wrap_in_channel(self, obj: Any, encoding: str, /):
return tp(shorthand=obj)
return obj

# A VariableParameter used directly as an encoding value is sugar for
# alt.datum[param], which generates a transform_calculate: datum[param_name].
if isinstance(obj, Parameter) and obj.param_type == "variable":
expr = GetItemExpression("datum", obj)
if channel := self.name_to_channel.get(encoding):
tp = channel["field"]
return tp(shorthand=expr)
return obj

if isinstance(obj, SchemaBase):
return obj
elif isinstance(obj, str):
Expand Down
11 changes: 10 additions & 1 deletion altair/vegalite/v6/schema/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import narwhals.stable.v1 as nw

from altair.expr.core import Expression as _Expression
from altair.expr.core import GetItemExpression
from altair.utils import infer_encoding_types as _infer_encoding_types
from altair.utils import parse_shorthand
from altair.utils.schemapi import Undefined, _infer_expr_type, with_property_setters
Expand Down Expand Up @@ -186,7 +187,15 @@ def to_dict( # noqa: C901
for sh in shorthand
]

shorthand_or_kwds = shorthand
from altair.vegalite.v6.api import Parameter

def _param_to_expr(val: Any) -> Any:
# Convert a VariableParameter to a datum[param] expression.
if isinstance(val, Parameter) and val.param_type == "variable":
return GetItemExpression("datum", val)
return val

shorthand_or_kwds = _param_to_expr(shorthand)

if isinstance(shorthand_or_kwds, (_Expression, core.ExprRef)):
vega_expr = (
Expand Down
1 change: 1 addition & 0 deletions doc/user_guide/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Encoding Channels
FillOpacityDatum
FillOpacityValue
FillValue
GetItemExpression
Href
HrefValue
Key
Expand Down
7 changes: 7 additions & 0 deletions tests/expr/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pytest
from jsonschema.exceptions import ValidationError

import altair as alt
from altair import datum, expr, ExprRef
from altair.expr import _ExprMeta
from altair.expr.core import Expression, GetAttrExpression
Expand Down Expand Up @@ -167,6 +168,12 @@ def test_expression_getitem():
assert repr(x) == "datum.foo[0]"


def test_datum_getitem_param():
xcol = alt.param(name="xcol", value="Horsepower")
x = datum[xcol]
assert repr(x) == "datum[xcol]"


def test_expression_function_expr():
# test including an expr.<CONSTANT> should return an ExprRef
er = expr(expr.PI * 2)
Expand Down
54 changes: 49 additions & 5 deletions tests/vegalite/v6/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2240,6 +2240,37 @@ def test_inline_calc_deduplication():
assert spec["encoding"]["y"]["field"] == field_name


def test_inline_calc_variable_parameter_as_shorthand():
"""A VariableParameter used as encoding value expands to datum[param_name]."""
param = alt.param(name="xcol", value="x")
chart = (
alt.Chart({"values": [{"x": 1}]})
.mark_point()
.encode(x=alt.X(param, type="quantitative"))
.add_params(param)
)
spec = chart.to_dict()
transforms = spec.get("transform", [])
assert len(transforms) == 1
assert transforms[0]["calculate"] == "datum[xcol]"
field_name = transforms[0]["as"]
assert spec["encoding"]["x"]["field"] == field_name


def test_inline_calc_variable_parameter_bare_channel_value():
"""A VariableParameter used directly as x=param also expands to datum[param_name]."""
param = alt.param(name="xcol", value="x")
chart = (
alt.Chart({"values": [{"x": 1}]}).mark_point().encode(x=param).add_params(param)
)
spec = chart.to_dict()
transforms = spec.get("transform", [])
assert len(transforms) == 1
assert transforms[0]["calculate"] == "datum[xcol]"
field_name = transforms[0]["as"]
assert spec["encoding"]["x"]["field"] == field_name


def test_inline_calc_explicit_type_overrides_inferred():
"""An explicit type on the channel overrides type inference."""
expr = alt.datum.x + alt.datum.y # would infer "quantitative"
Expand Down Expand Up @@ -2273,6 +2304,19 @@ def test_inline_calc_exprref_string_syntax():
field_name = transforms[0]["as"]
assert spec["encoding"]["x"]["field"] == field_name
assert spec["encoding"]["x"]["type"] == "quantitative"


def test_inline_calc_title_none_respected_for_variable_parameter():
"""Explicit title(None) should be preserved for inline variable-parameter channels."""
xcol_param = alt.param(name="xcol", value="x")
chart = (
alt.Chart(pd.DataFrame({"x": [1, 2, 3]}))
.mark_point()
.encode(x=alt.X(xcol_param).type("quantitative").title(None))
.add_params(xcol_param)
)

spec = chart.to_dict()
assert spec["encoding"]["x"]["title"] is None


Expand Down Expand Up @@ -2316,7 +2360,7 @@ def test_inline_calc_datum_expr_with_explicit_type_serializes_standard_type():


def test_inline_calc_does_not_rewrite_datum_expression_channels():
"""datum=ExprRef channels should stay datum-based (not auto-calc field rewrites)."""
"""Datum expression channels should stay datum-based (not auto-calc rewrites)."""
chart = (
alt.Chart()
.mark_rule()
Expand All @@ -2330,15 +2374,15 @@ def test_inline_calc_does_not_rewrite_datum_expression_channels():
spec = chart.to_dict()

assert spec["encoding"]["x"] == {
"datum": {"expr": "domain('x')[0]"},
"datum": {"expr": "domain('x',null)[0]"},
"type": "quantitative",
}
assert spec["encoding"]["y"] == {
"datum": {"expr": "domain('x')[0]"},
"datum": {"expr": "domain('x',null)[0]"},
"type": "quantitative",
}
assert spec["encoding"]["x2"] == {"datum": {"expr": "domain('x')[1]"}}
assert spec["encoding"]["y2"] == {"datum": {"expr": "domain('x')[1]"}}
assert spec["encoding"]["x2"] == {"datum": {"expr": "domain('x',null)[1]"}}
assert spec["encoding"]["y2"] == {"datum": {"expr": "domain('x',null)[1]"}}


def test_datum_channel_accepts_expression_objects_without_alt_expr_wrapper():
Expand Down
12 changes: 10 additions & 2 deletions tools/generate_schema_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,15 @@ def to_dict( # noqa: C901
for sh in shorthand
]

shorthand_or_kwds = shorthand
from altair.vegalite.v6.api import Parameter

def _param_to_expr(val: Any) -> Any:
# Convert a VariableParameter to a datum[param] expression.
if isinstance(val, Parameter) and val.param_type == "variable":
return GetItemExpression("datum", val)
return val

shorthand_or_kwds = _param_to_expr(shorthand)

if isinstance(shorthand_or_kwds, (_Expression, core.ExprRef)):
vega_expr = (
Expand Down Expand Up @@ -920,7 +928,7 @@ def generate_vegalite_channel_wrappers(fp: Path, /) -> ModuleDef[list[str]]:
"import sys",
"from typing import Any, overload, Literal, Union, TypedDict",
"import narwhals.stable.v1 as nw",
"from altair.expr.core import Expression as _Expression",
"from altair.expr.core import Expression as _Expression, GetItemExpression",
"from altair.utils import infer_encoding_types as _infer_encoding_types",
"from altair.utils import parse_shorthand",
"from altair.utils.schemapi import Undefined, _infer_expr_type, with_property_setters",
Expand Down