From 5bdb33d40b16efae2844e1e54ae861bdcca1d72e Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:07:44 +0000 Subject: [PATCH 01/18] chore(typing): Add temporary alias for `filter` --- altair/vegalite/v5/api.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index a3070acbf..f7de6ae2e 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -530,6 +530,16 @@ def check_fields_and_encodings(parameter: Parameter, field_name: str) -> bool: ] """Permitted types for `predicate`.""" +# FIXME: Remove before merging +_OrigFilterType: TypeAlias = "str | Expr | Expression | Predicate | Parameter | PredicateComposition | dict[str, Predicate | str | list | bool]" +"""**Temporary** alias for ``transform_filter``'s original annotation. + +Notes +----- +- Quite similar to ``_PredicateType`` +- Probably some redundant typing, that can be reduced +""" + _ComposablePredicateType: TypeAlias = Union[ _expr_core.OperatorMixin, SelectionPredicateComposition ] @@ -2949,13 +2959,7 @@ def transform_extent( # # E.g. {'not': alt.FieldRangePredicate(field='year', range=[1950, 1960])} def transform_filter( self, - filter: str - | Expr - | Expression - | Predicate - | Parameter - | PredicateComposition - | dict[str, Predicate | str | list | bool], + filter: _OrigFilterType, **kwargs: Any, ) -> Self: """ From 2efd5f81ccd1c3d5a51a8e3295c75186a3f42b60 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:10:21 +0000 Subject: [PATCH 02/18] refactor: Make `empty` a regular keyword arg - Not planning to keep the new body of the method - Purely fishing for the first test failure --- altair/vegalite/v5/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index f7de6ae2e..491ac96c4 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -2960,6 +2960,8 @@ def transform_extent( def transform_filter( self, filter: _OrigFilterType, + *, + empty: Optional[bool] = Undefined, **kwargs: Any, ) -> Self: """ @@ -2982,8 +2984,8 @@ def transform_filter( """ if isinstance(filter, Parameter): new_filter: dict[str, Any] = {"param": filter.name} - if "empty" in kwargs: - new_filter["empty"] = kwargs.pop("empty") + if not utils.is_undefined(empty): + new_filter["empty"] = empty elif isinstance(filter.empty, bool): new_filter["empty"] = filter.empty filter = new_filter From d2868f83a8e064c893e1a9277660493e2fe61dfb Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:15:51 +0000 Subject: [PATCH 03/18] refactor: Remove `**kwargs` Interestingly, this doesn't break any tests, Would indicate what I suspected in (#3657) was true --- altair/vegalite/v5/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 491ac96c4..a30f123df 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -2962,7 +2962,6 @@ def transform_filter( filter: _OrigFilterType, *, empty: Optional[bool] = Undefined, - **kwargs: Any, ) -> Self: """ Add a :class:`FilterTransform` to the schema. @@ -2989,7 +2988,7 @@ def transform_filter( elif isinstance(filter.empty, bool): new_filter["empty"] = filter.empty filter = new_filter - return self._add_transform(core.FilterTransform(filter=filter, **kwargs)) + return self._add_transform(core.FilterTransform(filter=filter)) def transform_flatten( self, From 841e88799def3c232e1338b9ef9cbd729a95ccdb Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:13:27 +0000 Subject: [PATCH 04/18] docs: Add note on `Predicate` Remember discovering this during (#3427), but hadn't documented --- altair/vegalite/v5/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index a30f123df..1a7675ce8 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -538,6 +538,8 @@ def check_fields_and_encodings(parameter: Parameter, field_name: str) -> bool: ----- - Quite similar to ``_PredicateType`` - Probably some redundant typing, that can be reduced + - ``Predicate`` derives ``PredicateComposition`` + - Includes all ``(Logical|Field|Parameter)...Predicate`` """ _ComposablePredicateType: TypeAlias = Union[ From 63de259f00594902ae5c79717ce471a7ce4ba05a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:24:11 +0000 Subject: [PATCH 05/18] feat(DRAFT): Adds `_transform_filter_impl` --- altair/vegalite/v5/api.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 1a7675ce8..21a675d37 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -609,6 +609,19 @@ def _predicate_to_condition( return condition +def _transform_filter_impl( + self: _TChart, filter: _PredicateType, *, empty: Optional[bool] = Undefined +) -> _TChart: + """ + Dummy implementation for ``TopLevelMixin.transform_filter``. + + **Not tested**, using to plan out how much of the ``(condition|when)`` logic can be reused. + """ + cond = _predicate_to_condition(filter, empty=empty) + pred = cond.get("test", cond) + return self._add_transform(core.FilterTransform(filter=pred)) + + def _condition_to_selection( condition: _Condition, if_true: _StatementType, @@ -5073,6 +5086,17 @@ def sphere() -> SphereGenerator: return core.SphereGenerator(sphere=True) +# NOTE: Copied directly from https://github.com/vega/altair/pull/3394#discussion_r1712993394 +_TChart = TypeVar( + "_TChart", + Chart, + RepeatChart, + ConcatChart, + HConcatChart, + VConcatChart, + FacetChart, + LayerChart, +) ChartType: TypeAlias = Union[ Chart, RepeatChart, ConcatChart, HConcatChart, VConcatChart, FacetChart, LayerChart ] From c22ba582d8635997921bb41d9443eaf69c0a0dde Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:11:21 +0000 Subject: [PATCH 06/18] feat: Adds `transform_filter` implementation Includes deprecation handling --- altair/vegalite/v5/api.py | 48 ++++++++++++--------------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 21a675d37..cd6800464 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -609,19 +609,6 @@ def _predicate_to_condition( return condition -def _transform_filter_impl( - self: _TChart, filter: _PredicateType, *, empty: Optional[bool] = Undefined -) -> _TChart: - """ - Dummy implementation for ``TopLevelMixin.transform_filter``. - - **Not tested**, using to plan out how much of the ``(condition|when)`` logic can be reused. - """ - cond = _predicate_to_condition(filter, empty=empty) - pred = cond.get("test", cond) - return self._add_transform(core.FilterTransform(filter=pred)) - - def _condition_to_selection( condition: _Condition, if_true: _StatementType, @@ -2974,9 +2961,10 @@ def transform_extent( # # E.g. {'not': alt.FieldRangePredicate(field='year', range=[1950, 1960])} def transform_filter( self, - filter: _OrigFilterType, - *, + predicate: Optional[_PredicateType] = Undefined, + *more_predicates: _ComposablePredicateType, empty: Optional[bool] = Undefined, + **constraints: _FieldEqualType, ) -> Self: """ Add a :class:`FilterTransform` to the schema. @@ -2996,14 +2984,17 @@ def transform_filter( self : Chart object returns chart to allow for chaining """ - if isinstance(filter, Parameter): - new_filter: dict[str, Any] = {"param": filter.name} - if not utils.is_undefined(empty): - new_filter["empty"] = empty - elif isinstance(filter.empty, bool): - new_filter["empty"] = filter.empty - filter = new_filter - return self._add_transform(core.FilterTransform(filter=filter)) + if depr_filter := constraints.pop("filter", None): + utils.deprecated_warn( + "Passing `filter` as a keyword is ambiguous.\n\n" + "Use a positional argument for `<5.5.0` behavior.\n" + "Or, `alt.datum['filter'] == ...` if referring to a column named 'filter'.", + version="5.5.0", + ) + more_predicates = *more_predicates, t.cast(Any, depr_filter) + cond = _parse_when(predicate, *more_predicates, empty=empty, **constraints) + pred = cond.get("test", cond) + return self._add_transform(core.FilterTransform(filter=pred)) def transform_flatten( self, @@ -5086,17 +5077,6 @@ def sphere() -> SphereGenerator: return core.SphereGenerator(sphere=True) -# NOTE: Copied directly from https://github.com/vega/altair/pull/3394#discussion_r1712993394 -_TChart = TypeVar( - "_TChart", - Chart, - RepeatChart, - ConcatChart, - HConcatChart, - VConcatChart, - FacetChart, - LayerChart, -) ChartType: TypeAlias = Union[ Chart, RepeatChart, ConcatChart, HConcatChart, VConcatChart, FacetChart, LayerChart ] From fc672bccf1f15c310b0e882e63d07a4cfa3de48c Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:14:31 +0000 Subject: [PATCH 07/18] fix(DRAFT): Add temp ignore for `line_chart_with_cumsum_faceted` - Need to widen the definition of `_PredicateType` - To support this, we'll need to model with `TypedDict`(s) --- .../examples_arguments_syntax/line_chart_with_cumsum_faceted.py | 2 +- tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py b/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py index 5a0fdb743..68994b0a4 100644 --- a/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py +++ b/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py @@ -12,7 +12,7 @@ columns_sorted = ['Drought', 'Epidemic', 'Earthquake', 'Flood'] alt.Chart(source).transform_filter( - {'and': [ + {'and': [ # type: ignore[arg-type] alt.FieldOneOfPredicate(field='Entity', oneOf=columns_sorted), # Filter data to show only disasters in columns_sorted alt.FieldRangePredicate(field='Year', range=[1900, 2000]) # Filter data to show only 20th century ]} diff --git a/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py b/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py index d9d887ba5..459cf6011 100644 --- a/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py +++ b/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py @@ -12,7 +12,7 @@ columns_sorted = ['Drought', 'Epidemic', 'Earthquake', 'Flood'] alt.Chart(source).transform_filter( - {'and': [ + {'and': [ # type: ignore[arg-type] alt.FieldOneOfPredicate(field='Entity', oneOf=columns_sorted), # Filter data to show only disasters in columns_sorted alt.FieldRangePredicate(field='Year', range=[1900, 2000]) # Filter data to show only 20th century ]} From 8554a46efff1900d3ac9cdac910549c8c6f53997 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:37:30 +0000 Subject: [PATCH 08/18] feat(typing): Widen `_FieldEqualType` to include `IntoExpression` This alias may be redundant now, need to review that later --- altair/vegalite/v5/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index cd6800464..d45286f94 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -562,7 +562,7 @@ def check_fields_and_encodings(parameter: Parameter, field_name: str) -> bool: """ -_FieldEqualType: TypeAlias = Union[PrimitiveValue_T, Map, Parameter, SchemaBase] +_FieldEqualType: TypeAlias = Union["IntoExpression", Parameter, SchemaBase] """Permitted types for equality checks on field values: - `datum.field == ...` From ff9d33fe773991a2db9f23d2c2e8b2e45f720908 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:56:00 +0000 Subject: [PATCH 09/18] fix: Try replacing `Undefined` first Didn't account for a `TypeError` that can be triggered if `more_predicates` isn't composable. E.g `filter={"field": "year", "oneOf": [1955, 2000]}` --- altair/vegalite/v5/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index d45286f94..f5b955229 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -2991,7 +2991,10 @@ def transform_filter( "Or, `alt.datum['filter'] == ...` if referring to a column named 'filter'.", version="5.5.0", ) - more_predicates = *more_predicates, t.cast(Any, depr_filter) + if utils.is_undefined(predicate): + predicate = t.cast(Any, depr_filter) + else: + more_predicates = *more_predicates, t.cast(Any, depr_filter) cond = _parse_when(predicate, *more_predicates, empty=empty, **constraints) pred = cond.get("test", cond) return self._add_transform(core.FilterTransform(filter=pred)) From b49703968d9ca2a3493eaa3f7b2e7a54ed0476e2 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:58:45 +0000 Subject: [PATCH 10/18] test: Add `(*predicates, **constraints)` syntax tests --- tests/vegalite/v5/test_api.py | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/vegalite/v5/test_api.py b/tests/vegalite/v5/test_api.py index ac184f86c..5e3e7a283 100644 --- a/tests/vegalite/v5/test_api.py +++ b/tests/vegalite/v5/test_api.py @@ -10,6 +10,7 @@ import re import sys import tempfile +import warnings from datetime import date, datetime from importlib.metadata import version as importlib_version from importlib.util import find_spec @@ -81,7 +82,7 @@ def _make_chart_type(chart_type): @pytest.fixture -def basic_chart(): +def basic_chart() -> alt.Chart: data = pd.DataFrame( { "a": ["A", "B", "C", "D", "E", "F", "G", "H", "I"], @@ -1189,6 +1190,64 @@ def test_filter_transform_selection_predicates(): ] +def test_filter_transform_predicates(basic_chart) -> None: + lhs, rhs = alt.datum["b"] >= 30, alt.datum["b"] < 60 + expected = [{"filter": lhs & rhs}] + actual = basic_chart.transform_filter(lhs, rhs).to_dict()["transform"] + assert actual == expected + + +def test_filter_transform_constraints(basic_chart) -> None: + lhs, rhs = alt.datum["a"] == "A", alt.datum["b"] == 30 + expected = [{"filter": lhs & rhs}] + actual = basic_chart.transform_filter(a="A", b=30).to_dict()["transform"] + assert actual == expected + + +def test_filter_transform_predicates_constraints(basic_chart) -> None: + from functools import reduce + from operator import and_ + + predicates = ( + alt.datum["a"] != "A", + alt.datum["a"] != "B", + alt.datum["a"] != "C", + alt.datum["b"] > 1, + alt.datum["b"] < 99, + ) + constraints = {"b": 30, "a": "D"} + pred_constraints = *predicates, alt.datum["b"] == 30, alt.datum["a"] != "D" + expected = [{"filter": reduce(and_, pred_constraints)}] + actual = basic_chart.transform_filter(*predicates, **constraints).to_dict()[ + "transform" + ] + assert actual == expected + + +def test_filter_transform_errors(basic_chart) -> None: + NO_ARGS = r"At least one.+Undefined" + FILTER_KWARGS = r"ambiguous" + + depr_filter = {"field": "year", "oneOf": [1955, 2000]} + expected = [{"filter": depr_filter}] + + with pytest.raises(TypeError, match=NO_ARGS): + basic_chart.transform_filter() + with pytest.raises(TypeError, match=NO_ARGS): + basic_chart.transform_filter(empty=True) + with pytest.raises(TypeError, match=NO_ARGS): + basic_chart.transform_filter(empty=False) + + with pytest.warns(alt.AltairDeprecationWarning, match=FILTER_KWARGS): + basic_chart.transform_filter(filter=depr_filter) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=alt.AltairDeprecationWarning) + actual = basic_chart.transform_filter(filter=depr_filter).to_dict()["transform"] + + assert actual == expected + + def test_resolve_methods(): chart = alt.LayerChart().resolve_axis(x="shared", y="independent") assert chart.resolve == alt.Resolve( From 7f6c188a299560a03eddc594294e9869ae79f0f9 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Sun, 3 Nov 2024 21:30:05 +0000 Subject: [PATCH 11/18] docs: Use `*predicates` in "Faceted Line Chart with Cumulative Sum" Makes use of #3668 Resolves https://github.com/vega/altair/pull/3664#discussion_r1823177593 --- .../line_chart_with_cumsum_faceted.py | 6 ++---- .../line_chart_with_cumsum_faceted.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py b/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py index 68994b0a4..d33df06ad 100644 --- a/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py +++ b/tests/examples_arguments_syntax/line_chart_with_cumsum_faceted.py @@ -12,10 +12,8 @@ columns_sorted = ['Drought', 'Epidemic', 'Earthquake', 'Flood'] alt.Chart(source).transform_filter( - {'and': [ # type: ignore[arg-type] - alt.FieldOneOfPredicate(field='Entity', oneOf=columns_sorted), # Filter data to show only disasters in columns_sorted - alt.FieldRangePredicate(field='Year', range=[1900, 2000]) # Filter data to show only 20th century - ]} + alt.FieldOneOfPredicate(field='Entity', oneOf=columns_sorted), + alt.FieldRangePredicate(field='Year', range=[1900, 2000]) ).transform_window( cumulative_deaths='sum(Deaths)', groupby=['Entity'] # Calculate cumulative sum of Deaths by Entity ).mark_line().encode( diff --git a/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py b/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py index 459cf6011..56dcdb931 100644 --- a/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py +++ b/tests/examples_methods_syntax/line_chart_with_cumsum_faceted.py @@ -12,10 +12,8 @@ columns_sorted = ['Drought', 'Epidemic', 'Earthquake', 'Flood'] alt.Chart(source).transform_filter( - {'and': [ # type: ignore[arg-type] - alt.FieldOneOfPredicate(field='Entity', oneOf=columns_sorted), # Filter data to show only disasters in columns_sorted - alt.FieldRangePredicate(field='Year', range=[1900, 2000]) # Filter data to show only 20th century - ]} + alt.FieldOneOfPredicate(field='Entity', oneOf=columns_sorted), + alt.FieldRangePredicate(field='Year', range=[1900, 2000]) ).transform_window( cumulative_deaths='sum(Deaths)', groupby=['Entity'] # Calculate cumulative sum of Deaths by Entity ).mark_line().encode( From be63d4e6adc9c50327be7e10421356b9d2447aa4 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:01:11 +0000 Subject: [PATCH 12/18] refactor: Remove `_OrigFilterType` --- altair/vegalite/v5/api.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 3f59b2443..8317d568b 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -128,7 +128,6 @@ NamedData, ParameterName, PointSelectionConfig, - Predicate, PredicateComposition, ProjectionType, RepeatMapping, @@ -523,18 +522,6 @@ def check_fields_and_encodings(parameter: Parameter, field_name: str) -> bool: ] """Permitted types for `predicate`.""" -# FIXME: Remove before merging -_OrigFilterType: TypeAlias = "str | Expr | Expression | Predicate | Parameter | PredicateComposition | dict[str, Predicate | str | list | bool]" -"""**Temporary** alias for ``transform_filter``'s original annotation. - -Notes ------ -- Quite similar to ``_PredicateType`` -- Probably some redundant typing, that can be reduced - - ``Predicate`` derives ``PredicateComposition`` - - Includes all ``(Logical|Field|Parameter)...Predicate`` -""" - _ComposablePredicateType: TypeAlias = Union[ _expr_core.OperatorMixin, core.PredicateComposition ] From 7a0cc42a99a0e2ba4f3e9c1e662ce7dbfeece5a2 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:23:55 +0000 Subject: [PATCH 13/18] docs: Update `.transform_filter()` docstring - Builds on the style introdcued for `alt.when` - Shows a few specific kinds of predicates - due to the prior doc listing 5 https://github.com/vega/altair/issues/3657 --- altair/vegalite/v5/api.py | 95 +++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 8317d568b..88a313d94 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -2987,8 +2987,6 @@ def transform_extent( """ return self._add_transform(core.ExtentTransform(extent=extent, param=param)) - # TODO: Update docstring - # # E.g. {'not': alt.FieldRangePredicate(field='year', range=[1950, 1960])} def transform_filter( self, predicate: Optional[_PredicateType] = Undefined, @@ -2997,22 +2995,91 @@ def transform_filter( **constraints: _FieldEqualType, ) -> Self: """ - Add a :class:`FilterTransform` to the schema. + Add a :class:`FilterTransform` to the spec. + + The resulting predicate is an ``&`` reduction over ``predicate`` and optional ``*``, ``**``, arguments. Parameters ---------- - filter : a filter expression or :class:`PredicateComposition` - The `filter` property must be one of the predicate definitions: - (1) a string or alt.expr expression - (2) a range predicate - (3) a selection predicate - (4) a logical operand combining (1)-(3) - (5) a Selection object + predicate + A selection or test predicate. ``str`` input will be treated as a test operand. + *more_predicates + Additional predicates, restricted to types supporting ``&``. + empty + For selection parameters, the predicate of empty selections returns ``True`` by default. + Override this behavior, with ``empty=False``. - Returns - ------- - self : Chart object - returns chart to allow for chaining + .. note:: + When ``predicate`` is a ``Parameter`` that is used more than once, + ``self.transform_filter(..., empty=...)`` provides granular control for each occurrence. + **constraints + Specify `Field Equal Predicate`_'s. + Shortcut for ``alt.datum.field_name == value``, see examples for usage. + + Warns + ----- + AltairDeprecationWarning + If called using ``filter`` as a keyword argument. + + See Also + -------- + alt.when : Uses a similar syntax for defining conditional values. + + Notes + ----- + - Directly inspired by the syntax used in `polars.DataFrame.filter`_. + + .. _Field Equal Predicate: + https://vega.github.io/vega-lite/docs/predicate.html#equal-predicate + .. _polars.DataFrame.filter: + https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.filter.html + + Examples + -------- + Setting up a common chart:: + + import altair as alt + from altair import datum + from vega_datasets import data + + source = data.population.url + chart = ( + alt.Chart(source) + .mark_line() + .encode( + x="age:O", + y="sum(people):Q", + color=alt.Color("year:O").legend(symbolType="square"), + ) + ) + chart + + Singular predicates can be expressed via ``datum``:: + + chart.transform_filter(datum.year <= 1980) + + We can also use parameter selections directly:: + + selection = alt.selection_point(encodings=["color"], bind="legend") + chart.transform_filter(selection).add_params(selection) + + Or using field predicates:: + + between_1950_60 = alt.FieldRangePredicate(field="year", range=[1950, 1960]) + chart.transform_filter(between_1950_60) | chart.transform_filter(~between_1950_60) + + Predicates can be composed together using logical operands:: + + chart.transform_filter(between_1950_60 | (datum.year == 1850)) + + Predicates passed as positional arguments will be reduced with ``&``:: + + chart.transform_filter(datum.year > 1980, datum.age != 90) + + Using keyword-argument ``constraints`` can simplify compositions like:: + + verbose_composition = chart.transform_filter((datum.year == 2000) & (datum.sex == 1)) + chart.transform_filter(year=2000, sex=1) """ if depr_filter := constraints.pop("filter", None): utils.deprecated_warn( From 08a4207eb5c8bea2632cef648acb75a535e1062d Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:55:28 +0000 Subject: [PATCH 14/18] docs: Minor corrections in examples --- altair/vegalite/v5/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 88a313d94..5a92ffe95 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -3058,12 +3058,12 @@ def transform_filter( chart.transform_filter(datum.year <= 1980) - We can also use parameter selections directly:: + We can also use selection parameters directly:: selection = alt.selection_point(encodings=["color"], bind="legend") chart.transform_filter(selection).add_params(selection) - Or using field predicates:: + Or a field predicate:: between_1950_60 = alt.FieldRangePredicate(field="year", range=[1950, 1960]) chart.transform_filter(between_1950_60) | chart.transform_filter(~between_1950_60) From 5fd4f5a355ddba87790767422df4339cd63a9499 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:05:26 +0000 Subject: [PATCH 15/18] refactor(typing): Cast deprecated `filter` in one location Need to forgo all type safety here, since it would permit `_PredicateType` instead of the narrower `_FieldEqualType` --- altair/vegalite/v5/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 5a92ffe95..15880fc85 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -3081,7 +3081,7 @@ def transform_filter( verbose_composition = chart.transform_filter((datum.year == 2000) & (datum.sex == 1)) chart.transform_filter(year=2000, sex=1) """ - if depr_filter := constraints.pop("filter", None): + if depr_filter := t.cast(Any, constraints.pop("filter", None)): utils.deprecated_warn( "Passing `filter` as a keyword is ambiguous.\n\n" "Use a positional argument for `<5.5.0` behavior.\n" @@ -3089,9 +3089,9 @@ def transform_filter( version="5.5.0", ) if utils.is_undefined(predicate): - predicate = t.cast(Any, depr_filter) + predicate = depr_filter else: - more_predicates = *more_predicates, t.cast(Any, depr_filter) + more_predicates = *more_predicates, depr_filter cond = _parse_when(predicate, *more_predicates, empty=empty, **constraints) pred = cond.get("test", cond) return self._add_transform(core.FilterTransform(filter=pred)) From d640933ce1db795a0c2dd3814f761d47cb221d96 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:08:05 +0000 Subject: [PATCH 16/18] refactor: Remove `pred` assignment I added this while trying to resolve a different typing issue, purely to see what was inferred --- altair/vegalite/v5/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 15880fc85..6eb6b7d63 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -3093,8 +3093,7 @@ def transform_filter( else: more_predicates = *more_predicates, depr_filter cond = _parse_when(predicate, *more_predicates, empty=empty, **constraints) - pred = cond.get("test", cond) - return self._add_transform(core.FilterTransform(filter=pred)) + return self._add_transform(core.FilterTransform(filter=cond.get("test", cond))) def transform_flatten( self, From 2d57d6ec74f868eb08811602475d7beb2a57fd8a Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:17:14 +0000 Subject: [PATCH 17/18] docs(typing): Update `_FieldEqualType` Think this makes it clearer how each of these align --- altair/vegalite/v5/api.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index 6eb6b7d63..115fe7e3c 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -543,11 +543,18 @@ def check_fields_and_encodings(parameter: Parameter, field_name: str) -> bool: _FieldEqualType: TypeAlias = Union["IntoExpression", Parameter, SchemaBase] -"""Permitted types for equality checks on field values: +""" +Permitted types for equality checks on field values. + +Applies to the following context(s): + + import altair as alt -- `datum.field == ...` -- `FieldEqualPredicate(equal=...)` -- `when(**constraints=...)` + alt.datum.field == ... + alt.FieldEqualPredicate(field="field", equal=...) + alt.when(field=...) + alt.when().then().when(field=...) + alt.Chart.transform_filter(field=...) """ From 3efc144c25be4acaf2b642f59c5ee23add88e383 Mon Sep 17 00:00:00 2001 From: dangotbanned <125183946+dangotbanned@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:55:03 +0000 Subject: [PATCH 18/18] docs: Adapt examples for user guide https://github.com/vega/altair/pull/3664#issuecomment-2473527759 --- doc/user_guide/transform/filter.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/doc/user_guide/transform/filter.rst b/doc/user_guide/transform/filter.rst index 39c268210..62ee6e334 100644 --- a/doc/user_guide/transform/filter.rst +++ b/doc/user_guide/transform/filter.rst @@ -20,6 +20,8 @@ expressions and objects: We'll show a brief example of each of these in the following sections +.. _filter-expression: + Filter Expression ^^^^^^^^^^^^^^^^^ A filter expression uses the `Vega expression`_ language, either specified @@ -189,12 +191,26 @@ Then, we can *invert* this selection using ``~``: chart.transform_filter(~between_1950_60) We can further refine our filter by *composing* multiple predicates together. -In this case, using ``alt.datum``: +In this case, using ``datum``: + +.. altair-plot:: + + chart.transform_filter(~between_1950_60 & (datum.age <= 70)) + +When passing multiple predicates they will be reduced with ``&``: .. altair-plot:: - chart.transform_filter(~between_1950_60 & (alt.datum.age <= 70)) + chart.transform_filter(datum.year > 1980, datum.age != 90) +Using keyword-argument ``constraints`` can simplify our first example in :ref:`filter-expression`: + +.. altair-plot:: + + alt.Chart(source).mark_area().encode( + x="age:O", + y="people:Q", + ).transform_filter(year=2000, sex=1) Transform Options ^^^^^^^^^^^^^^^^^