Repository: altair-viz/altair
Pull request #1599 (2019-07-05 15:19:18)

ENH: add selector value access for expressions
Addresses #1013 

This PR adds the ability to access selector values within filter & condition expressions; for example:
```python
import altair as alt
import pandas as pd
import numpy as np

df = pd.DataFrame({
    'xval': range(100),
    'yval': np.random.randn(100).cumsum()
})

slider = alt.binding_range(min=0, max=100, step=1, name='cutoff')
selector = alt.selection_single(fields=['cutoff'], bind=slider, init={'cutoff': 50})

alt.Chart(df).mark_point().encode(
    x='xval',
    y='yval',
    color=alt.condition(alt.datum.xval < selector.cutoff, alt.value('red'), alt.value('blue'))
).add_selection(
    selector
)
```
<img width="512" alt="Screen Shot 2019-07-05 at 8 18 50 AM" src="https://user-images.githubusercontent.com/781659/60731901-8d892280-9efd-11e9-8790-e7cedefc7508.png">


jakevdp: It would be useful to add an example and some documentation as well.

Patch:
diff --git a/altair/expr/core.py b/altair/expr/core.py
index 7f0b2fe5..4f0aa8ac 100644
--- a/altair/expr/core.py
+++ b/altair/expr/core.py
@@ -7,10 +7,10 @@ class DatumType(object):
         raise ValueError("must use datum.attribute")
 
     def __getattr__(self, attr):
-        return ValueExpression(attr)
+        return GetAttrExpression("datum", attr)
 
     def __getitem__(self, attr):
-        return GetItemExpression(attr)
+        return GetItemExpression("datum", attr)
 
 
 datum = DatumType()
@@ -169,17 +169,17 @@ class ConstExpression(Expression):
         return str(self.name)
 
 
-class ValueExpression(Expression):
-    def __init__(self, name):
-        super(ValueExpression, self).__init__(name=name)
+class GetAttrExpression(Expression):
+    def __init__(self, group, name):
+        super(GetAttrExpression, self).__init__(group=group, name=name)
 
     def __repr__(self):
-        return "datum.{}".format(self.name)
+        return "{}.{}".format(self.group, self.name)
 
 
 class GetItemExpression(Expression):
-    def __init__(self, name):
-        super(GetItemExpression, self).__init__(name=name)
+    def __init__(self, group, name):
+        super(GetItemExpression, self).__init__(group=group, name=name)
 
     def __repr__(self):
-        return "datum['{}']".format(self.name)
+        return "{}['{}']".format(self.group, self.name)
diff --git a/altair/vegalite/v3/api.py b/altair/vegalite/v3/api.py
index 2c35f375..f844ef0f 100644
--- a/altair/vegalite/v3/api.py
+++ b/altair/vegalite/v3/api.py
@@ -186,6 +186,12 @@ class Selection(object):
         if isinstance(other, Selection):
             other = other.name
         return core.SelectionOr(**{'or': [self.name, other]})
+   
+    def __getattr__(self, field_name):
+        return expr.core.GetAttrExpression(self.name, field_name)
+
+    def __getitem__(self, field_name):
+        return expr.core.GetItemExpression(self.name, field_name)
 
 
 # ------------------------------------------------------------------------
diff --git a/doc/user_guide/interactions.rst b/doc/user_guide/interactions.rst
index eca2b7cd..293ded86 100644
--- a/doc/user_guide/interactions.rst
+++ b/doc/user_guide/interactions.rst
@@ -411,6 +411,43 @@ With interval selections, the ``bind`` property can be set to the value of ``"sc
     )
     
 
+Selection Values in Expressions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Selection values can be accessed directly and used in expressions that affect the
+chart. For example, here we create a slider to choose a cutoff value, and color
+points based on whether they are smaller or larger than the value:
+
+.. altair-plot::
+
+   import altair as alt
+   import pandas as pd
+   import numpy as np
+
+   rand = np.random.RandomState(42)
+
+   df = pd.DataFrame({
+       'xval': range(100),
+       'yval': rand.randn(100).cumsum()
+   })
+
+   slider = alt.binding_range(min=0, max=100, step=1, name='cutoff:')
+   selector = alt.selection_single(name="SelectorName", fields=['cutoff'],
+                                   bind=slider, init={'cutoff': 50})
+ 
+   alt.Chart(df).mark_point().encode(
+       x='xval',
+       y='yval',
+       color=alt.condition(
+           alt.datum.xval < selector.cutoff,
+           alt.value('red'), alt.value('blue')
+       )
+   ).add_selection(
+       selector
+   )
+
+Selector values can be similarly used anywhere that expressions are valid, for
+example, in a :ref:`user-guide-calculate-transform` or a
+:ref:`user-guide-filter-transform`.
 
 
 Further Examples


Test patch:
diff --git a/altair/vegalite/v3/tests/test_api.py b/altair/vegalite/v3/tests/test_api.py
index 3330d106..cae64b5e 100644
--- a/altair/vegalite/v3/tests/test_api.py
+++ b/altair/vegalite/v3/tests/test_api.py
@@ -188,6 +188,16 @@ def test_selection_to_dict():
     ).to_dict()
 
 
+def test_selection_expression():
+    selection = alt.selection_single(fields=['value'])
+
+    assert isinstance(selection.value, alt.expr.Expression)
+    assert selection.value.to_dict() == '{0}.value'.format(selection.name)
+
+    assert isinstance(selection['value'], alt.expr.Expression)
+    assert selection['value'].to_dict() == "{0}['value']".format(selection.name)
+
+
 @pytest.mark.parametrize('format', ['html', 'json', 'png', 'svg'])
 @pytest.mark.skipif('not selenium')
 def test_save(format, basic_chart):