Repository: altair-viz/altair

Issue:
ENH: encodings with multiple fields (e.g. tooltip)
We need to update the ``encode()`` method so that it will handle multiple encoding fields in cases where it is supported.

For example, in the most recent vega-lite release, it is possible to pass multiple fields to the tooltip encoding:
```python
from vega_datasets import data
cars = data.cars()

chart = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin'
)

chart.encoding.tooltip = [{'field': 'Name', 'type': 'nominal'},
                          {'field': 'Origin', 'type': 'nominal'}]

chart
```
<img width="534" alt="screen shot 2018-05-08 at 10 37 35 am" src="https://user-images.githubusercontent.com/781659/39773111-e8c2f8dc-52ab-11e8-9d4b-55c7eb4f31b5.png">

Altair should make this available via a more simplified API, such as
```python
from vega_datasets import data
cars = data.cars()

chart = alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin',
    tooltip=['Name', 'Origin']
)
```

Discussion:

Patch:
diff --git a/altair/sphinxext/altairgallery.py b/altair/sphinxext/altairgallery.py
index a413bf98..6309cda2 100644
--- a/altair/sphinxext/altairgallery.py
+++ b/altair/sphinxext/altairgallery.py
@@ -34,12 +34,6 @@ This gallery contains a selection of examples of the plots Altair can create.
 
 Some may seem fairly complicated at first glance, but they are built by combining a simple set of declarative building blocks.
 
-Many draw upon sample datasets compiled by the `Vega <https://vega.github.io/vega/>`_ project. To access them yourself, install `vega_datasets <https://github.com/altair-viz/vega_datasets>`_.
-
-.. code-block::
-
-   $ pip install vega_datasets
-
 {% for grouper, group in examples %}
 
 .. _gallery-category-{{ grouper }}:
diff --git a/altair/vegalite/v1/schema/channels.py b/altair/vegalite/v1/schema/channels.py
index 046d99d9..500a36d7 100644
--- a/altair/vegalite/v1/schema/channels.py
+++ b/altair/vegalite/v1/schema/channels.py
@@ -15,6 +15,12 @@ class FieldChannelMixin(object):
         context = context or {}
         if self.shorthand is Undefined:
             kwds = {}
+        elif isinstance(self.shorthand, (tuple, list)):
+            # If given a list of shorthands, then transform it to a list of classes
+            kwds = self._kwds.copy()
+            kwds.pop('shorthand')
+            return [self.__class__(shorthand, **kwds).to_dict()
+                    for shorthand in self.shorthand]
         elif isinstance(self.shorthand, six.string_types):
             kwds = parse_shorthand(self.shorthand, data=context.get('data', None))
             type_defined = self._kwds.get('type', Undefined) is not Undefined
diff --git a/altair/vegalite/v2/api.py b/altair/vegalite/v2/api.py
index 557fe897..5a94eb4b 100644
--- a/altair/vegalite/v2/api.py
+++ b/altair/vegalite/v2/api.py
@@ -860,9 +860,14 @@ class EncodingMixin(object):
     def encode(self, *args, **kwargs):
         # First convert args to kwargs by inferring the class from the argument
         if args:
-            mapping = _get_channels_mapping()
+            channels_mapping = _get_channels_mapping()
             for arg in args:
-                encoding = mapping.get(type(arg), None)
+                if isinstance(arg, (list, tuple)) and len(arg) > 0:
+                    type_ = type(arg[0])
+                else:
+                    type_ = type(arg)
+
+                encoding = channels_mapping.get(type_, None)
                 if encoding is None:
                     raise NotImplementedError("non-keyword arg of type {0}"
                                               "".format(type(arg)))
@@ -880,6 +885,9 @@ class EncodingMixin(object):
             if isinstance(obj, six.string_types):
                 obj = {'shorthand': obj}
 
+            if isinstance(obj, (list, tuple)):
+                return [_wrap_in_channel_class(subobj, prop) for subobj in obj]
+
             if 'value' in obj:
                 clsname += 'Value'
 
diff --git a/altair/vegalite/v2/examples/simple_scatter_with_errorbars.py b/altair/vegalite/v2/examples/simple_scatter_with_errorbars.py
deleted file mode 100644
index 9118e497..00000000
--- a/altair/vegalite/v2/examples/simple_scatter_with_errorbars.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""
-Simple Scatter Plot with Errorbars
-----------------------------------
-
-A simple scatter plot of a data set with errorbars.
-
-"""
-# category: scatter plots
-
-import altair as alt
-
-import numpy as np
-import pandas as pd
-
-# generate some data points with uncertainties
-np.random.seed(0)
-x = [1, 2, 3, 4, 5]
-y = np.random.normal(10, 0.5, size=len(x))
-yerr = 0.2
-
-# set up data frame
-data = pd.DataFrame({"x":x, "y":y, "yerr":yerr})
-
-# generate the points
-points = alt.Chart(data).mark_point(filled=True, size=50).encode(
-    alt.X("x", 
-          scale=alt.Scale(domain=(0,6)),
-          axis=alt.Axis(title='x')
-    ),
-    y=alt.Y('y', 
-            scale=alt.Scale(zero=False, domain=(10, 11)),
-            axis=alt.Axis(title="y")),
-    color=alt.value('black')
-)
-
-# generate the error bars
-errorbars = alt.Chart(data).mark_rule().encode(
-    x=alt.X("x"),
-    y="ymin:Q",
-    y2="ymax:Q"
-).transform_calculate(
-    ymin="datum.y-datum.yerr",
-    ymax="datum.y+datum.yerr"
-)
-
-points + errorbars
diff --git a/altair/vegalite/v2/examples/stem_and_leaf.py b/altair/vegalite/v2/examples/stem_and_leaf.py
index dd3d328d..604fe837 100644
--- a/altair/vegalite/v2/examples/stem_and_leaf.py
+++ b/altair/vegalite/v2/examples/stem_and_leaf.py
@@ -1,7 +1,7 @@
 """
-Stem and Leaf Plot
-------------------
-This example shows how to make a stem and leaf plot.
+Steam and Leaf Plot
+-------------------
+This example shows how to make a steam and leaf plot.
 """
 # category: other charts
 import altair as alt
@@ -12,7 +12,7 @@ np.random.seed(42)
 # Generating random data
 original_data = pd.DataFrame({'samples': np.array(np.random.normal(50, 15, 100), dtype=np.int)})
 
-# Splitting stem and leaf
+# Splitting steam and leaf
 original_data['stem'] = original_data['samples'].apply(lambda x: str(x)[:-1])
 original_data['leaf'] = original_data['samples'].apply(lambda x: str(x)[-1])
 
diff --git a/altair/vegalite/v2/schema/channels.py b/altair/vegalite/v2/schema/channels.py
index bc9c40e7..b780dd88 100644
--- a/altair/vegalite/v2/schema/channels.py
+++ b/altair/vegalite/v2/schema/channels.py
@@ -15,6 +15,12 @@ class FieldChannelMixin(object):
         context = context or {}
         if self.shorthand is Undefined:
             kwds = {}
+        elif isinstance(self.shorthand, (tuple, list)):
+            # If given a list of shorthands, then transform it to a list of classes
+            kwds = self._kwds.copy()
+            kwds.pop('shorthand')
+            return [self.__class__(shorthand, **kwds).to_dict()
+                    for shorthand in self.shorthand]
         elif isinstance(self.shorthand, six.string_types):
             kwds = parse_shorthand(self.shorthand, data=context.get('data', None))
             type_defined = self._kwds.get('type', Undefined) is not Undefined
diff --git a/doc/user_guide/saving_charts.rst b/doc/user_guide/saving_charts.rst
index cddaefa4..308530a6 100644
--- a/doc/user_guide/saving_charts.rst
+++ b/doc/user_guide/saving_charts.rst
@@ -74,7 +74,7 @@ For example, here we save a simple scatter-plot to JSON:
 
     chart.save('chart.json')
 
-The contents of the resulting file will look something like this:
+The contetns of the resulting file will look something like this:
 
 .. code-block:: json
 
diff --git a/setup.py b/setup.py
index 696329a4..92c40447 100644
--- a/setup.py
+++ b/setup.py
@@ -126,13 +126,12 @@ setup(name=NAME,
         'dev': DEV_REQUIRES
       },
       classifiers=[
-        'Development Status :: 5 - Production/Stable',
+        'Development Status :: 4 - Beta',
         'Environment :: Console',
         'Intended Audience :: Science/Research',
         'License :: OSI Approved :: BSD License',
         'Natural Language :: English',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6'],
+        'Programming Language :: Python :: 3.5'],
      )
diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py
index 0caae349..c0ad0aad 100644
--- a/tools/generate_schema_wrapper.py
+++ b/tools/generate_schema_wrapper.py
@@ -68,6 +68,12 @@ class FieldChannelMixin(object):
         context = context or {}
         if self.shorthand is Undefined:
             kwds = {}
+        elif isinstance(self.shorthand, (tuple, list)):
+            # If given a list of shorthands, then transform it to a list of classes
+            kwds = self._kwds.copy()
+            kwds.pop('shorthand')
+            return [self.__class__(shorthand, **kwds).to_dict()
+                    for shorthand in self.shorthand]
         elif isinstance(self.shorthand, six.string_types):
             kwds = parse_shorthand(self.shorthand, data=context.get('data', None))
             type_defined = self._kwds.get('type', Undefined) is not Undefined


Test patch:
diff --git a/altair/vegalite/v2/tests/test_api.py b/altair/vegalite/v2/tests/test_api.py
index 1b78f112..b78e6963 100644
--- a/altair/vegalite/v2/tests/test_api.py
+++ b/altair/vegalite/v2/tests/test_api.py
@@ -90,6 +90,30 @@ def test_chart_infer_types():
     assert dct['encoding']['y']['type'] == 'ordinal'
 
 
+def test_multiple_encodings():
+    encoding_dct = [{'field': 'value', 'type': 'quantitative'},
+                    {'field': 'name', 'type': 'nominal'}]
+    chart1 = alt.Chart('data.csv').mark_point().encode(
+        detail=['value:Q', 'name:N'],
+        tooltip=['value:Q', 'name:N']
+    )
+
+    chart2 = alt.Chart('data.csv').mark_point().encode(
+        alt.Detail(['value:Q', 'name:N']),
+        alt.Tooltip(['value:Q', 'name:N'])
+    )
+
+    chart3 = alt.Chart('data.csv').mark_point().encode(
+        [alt.Detail('value:Q'), alt.Detail('name:N')],
+        [alt.Tooltip('value:Q'), alt.Tooltip('name:N')]
+    )
+
+    for chart in [chart1, chart2, chart3]:
+        dct = chart.to_dict()
+        assert dct['encoding']['detail'] == encoding_dct
+        assert dct['encoding']['tooltip'] == encoding_dct
+
+
 def test_chart_operations():
     data = pd.DataFrame({'x': pd.date_range('2012', periods=10, freq='Y'),
                          'y': range(10),