Repository: vega/altair

Issue:
LookupData doesn't handle DataFrame
A student was trying to do a geographic map with countries mapped to color. He found that he couldn't get this example:

https://altair-viz.github.io/gallery/choropleth.html

working with a DataFrame rather than a URL. The issue is that our `LookupData` object doesn't use the data transformer pipeline to properly handle a DataFrame argument. The following code did work:

```python
countries = alt.topo_feature(data.world_110m.url,'countries')
country_ses = pd.read_csv('/data/country_ses.csv')
country_values = alt.pipe(country_ses[['id', 'ses']], alt.to_values)

chart = alt.Chart(countries).mark_geoshape().properties(
    projection={'type': 'equirectangular'},
    width=500,
    height=300
).encode(
    color='ses:Q'
).transform_lookup(
    lookup='id',
    from_=alt.LookupData(country_values, 'id', ['ses'])
)
chart
```

Should be an easy fix for `LookupData` to use the data transformers. Are there other parts of the API where we are not fully handling DataFrames?
LookupData doesn't handle DataFrame
A student was trying to do a geographic map with countries mapped to color. He found that he couldn't get this example:

https://altair-viz.github.io/gallery/choropleth.html

working with a DataFrame rather than a URL. The issue is that our `LookupData` object doesn't use the data transformer pipeline to properly handle a DataFrame argument. The following code did work:

```python
countries = alt.topo_feature(data.world_110m.url,'countries')
country_ses = pd.read_csv('/data/country_ses.csv')
country_values = alt.pipe(country_ses[['id', 'ses']], alt.to_values)

chart = alt.Chart(countries).mark_geoshape().properties(
    projection={'type': 'equirectangular'},
    width=500,
    height=300
).encode(
    color='ses:Q'
).transform_lookup(
    lookup='id',
    from_=alt.LookupData(country_values, 'id', ['ses'])
)
chart
```

Should be an easy fix for `LookupData` to use the data transformers. Are there other parts of the API where we are not fully handling DataFrames?


Discussion:
Good catch... I think what we need is for ``LookupData()`` to handle data inputs the same way that top-level methods do.

I'm not sure if there are other places where this would come up, but I'll take a look through the schema to see if I can find any.
It looks like ``LookupData`` is the only other schema definition beyond top-level objects that has a general data property.
nice, glad we found it!

On Wed, Mar 21, 2018 at 10:55 AM, Jake Vanderplas <notifications@github.com>
wrote:

> It looks like LookupData is the only other schema definition beyond
> top-level objects that has a general data property.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/altair-viz/altair/issues/626#issuecomment-375038202>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AABr0MPQSBprarn-v9wvJWcqbrSv21R3ks5tgpQIgaJpZM4S0u2d>
> .
>



-- 
Brian E. Granger
Associate Professor of Physics and Data Science
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
bgranger@calpoly.edu and ellisonbg@gmail.com

Good catch... I think what we need is for ``LookupData()`` to handle data inputs the same way that top-level methods do.

I'm not sure if there are other places where this would come up, but I'll take a look through the schema to see if I can find any.
It looks like ``LookupData`` is the only other schema definition beyond top-level objects that has a general data property.
nice, glad we found it!

On Wed, Mar 21, 2018 at 10:55 AM, Jake Vanderplas <notifications@github.com>
wrote:

> It looks like LookupData is the only other schema definition beyond
> top-level objects that has a general data property.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/altair-viz/altair/issues/626#issuecomment-375038202>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AABr0MPQSBprarn-v9wvJWcqbrSv21R3ks5tgpQIgaJpZM4S0u2d>
> .
>



-- 
Brian E. Granger
Associate Professor of Physics and Data Science
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
bgranger@calpoly.edu and ellisonbg@gmail.com


Resolving patch:
diff --git a/altair/vegalite/v2/api.py b/altair/vegalite/v2/api.py
--- a/altair/vegalite/v2/api.py
+++ b/altair/vegalite/v2/api.py
@@ -14,11 +14,37 @@
 
 SCHEMA_URL = "https://vega.github.io/schema/vega-lite/v2.json"
 
+#------------------------------------------------------------------------
+# Data Utilities
+def _prepare_data(data):
+    """Convert input data to data for use within schema"""
+    if data is Undefined:
+        return data
+    elif isinstance(data, (dict, core.Data, core.InlineData,
+                         core.UrlData, core.NamedData)):
+        return data
+    elif isinstance(data, pd.DataFrame):
+        return pipe(data, data_transformers.get())
+    elif isinstance(data, six.string_types):
+        return core.UrlData(data)
+    else:
+        warnings.warn("data of type {0} not recognized".format(type(data)))
+        return data
+
 
 #------------------------------------------------------------------------
-# Aliases
+# Aliases & specializations
 Bin = core.BinParams
 
+@utils.use_signature(core.LookupData)
+class LookupData(core.LookupData):
+    def to_dict(self, *args, **kwargs):
+        """Convert the chart to a dictionary suitable for JSON export"""
+        copy = self.copy(ignore=['data'])
+        copy.data = _prepare_data(copy.data)
+        return super(LookupData, copy).to_dict(*args, **kwargs)
+
+
 #------------------------------------------------------------------------
 # Encoding will contain channel objects that aren't valid at instantiation
 core.EncodingWithFacet._class_is_valid_at_instantiation = False
@@ -264,20 +290,11 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
     _default_spec_values = {"config": {"view": {"width": 400, "height": 300}}}
     _class_is_valid_at_instantiation = False
 
-    def _prepare_data(self):
-        if isinstance(self.data, (dict, core.Data, core.InlineData,
-                                  core.UrlData, core.NamedData)):
-            pass
-        elif isinstance(self.data, pd.DataFrame):
-            self.data = pipe(self.data, data_transformers.get())
-        elif isinstance(self.data, six.string_types):
-            self.data = core.UrlData(self.data)
-
     def to_dict(self, *args, **kwargs):
         """Convert the chart to a dictionary suitable for JSON export"""
         copy = self.copy()
         original_data = getattr(copy, 'data', Undefined)
-        copy._prepare_data()
+        copy.data = _prepare_data(original_data)
 
         # We make use of two context markers:
         # - 'data' points to the data that should be referenced for column type


Test patch:
diff --git a/altair/vegalite/v2/tests/test_api.py b/altair/vegalite/v2/tests/test_api.py
--- a/altair/vegalite/v2/tests/test_api.py
+++ b/altair/vegalite/v2/tests/test_api.py
@@ -263,3 +263,13 @@ def test_resolve_methods():
     with pytest.raises(ValueError) as err:
         alt.Chart().resolve_axis(x='shared')
     assert str(err.value).endswith("object has no attribute 'resolve'")
+
+
+def test_LookupData():
+    df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
+    lookup = alt.LookupData(data=df, key='x')
+    dct = lookup.to_dict()
+    assert dct['key'] == 'x'
+    assert dct['data'] == {'values': [{'x': 1, 'y': 4},
+                                      {'x': 2, 'y': 5},
+                                      {'x': 3, 'y': 6}]}