Repository: altair-viz/altair
Pull request #690 (2018-03-31 17:25:37)

WIP: Transform docs
Adding more rigorous documentation of available transforms.


Patch:
diff --git a/altair/expr/core.py b/altair/expr/core.py
index bcc04efb..314b7540 100644
--- a/altair/expr/core.py
+++ b/altair/expr/core.py
@@ -63,23 +63,15 @@ class Expression(SchemaBase):
     def __rmul__(self, other):
         return BinaryExpression("*", other, self)
 
-    def __floordiv__(self, other):
-        return BinaryExpression("/", self, other)
-
-    def __rfloordiv__(self, other):
-        return BinaryExpression("/", other, self)
-
     def __truediv__(self, other):
-        warnings.warn("Javascript uses floor division; use '//' to silence this warning")
-        return self.__floordiv__(other)
+        return BinaryExpression("/", self, other)
 
     def __rtruediv__(self, other):
-        warnings.warn("Javascript uses floor division; use '//' to silence this warning")
-        return self.__rfloordiv(other)
+        return BinaryExpression("/", other, self)
 
-    __div__ = __floordiv__
+    __div__ = __truediv__
 
-    __rdiv__ = __rfloordiv__
+    __rdiv__ = __rtruediv__
 
     def __mod__(self, other):
         return BinaryExpression('%', self, other)
diff --git a/altair/vegalite/v2/api.py b/altair/vegalite/v2/api.py
index 9ce7b308..09925f4c 100644
--- a/altair/vegalite/v2/api.py
+++ b/altair/vegalite/v2/api.py
@@ -84,7 +84,11 @@ def _get_channels_mapping():
 # -------------------------------------------------------------------------
 # Tools for working with selections
 class SelectionMapping(core.VegaLiteSchema):
-    """A mapping of selection names to selection definitions"""
+    """A mapping of selection names to selection definitions.
+
+    This is designed to match the schema of the "selection" property of
+    top-level objects.
+    """
     _schema = {
         'type': 'object',
         'additionalPropeties': {'$ref': '#/definitions/SelectionDef'}
@@ -126,6 +130,7 @@ class NamedSelection(SelectionMapping):
 
         Examples
         --------
+        >>> import altair as alt
         >>> sel = alt.selection_interval(name='interval')
         >>> sel.ref()
         {'selection': 'interval'}
@@ -136,12 +141,12 @@ class NamedSelection(SelectionMapping):
         return core.SelectionNot(**{'not': self._get_name()})
 
     def __and__(self, other):
-        if isinstance(other, SelectionMapping):
+        if isinstance(other, NamedSelection):
             other = other._get_name()
         return core.SelectionAnd(**{'and': [self._get_name(), other]})
 
     def __or__(self, other):
-        if isinstance(other, SelectionMapping):
+        if isinstance(other, NamedSelection):
             other = other._get_name()
         return core.SelectionOr(**{'or': [self._get_name(), other]})
 
@@ -457,32 +462,77 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
             copy.transform.extend(transforms)
         return copy
 
-    def transform_aggregate(self, aggregate, groupby=Undefined, **kwargs):
+    def transform_aggregate(self, aggregate=Undefined, groupby=Undefined, **kwds):
         """
         Add an AggregateTransform to the schema.
 
-        Attributes
+        Parameters
         ----------
         aggregate : List(AggregatedFieldDef)
             Array of objects that define fields to aggregate.
         groupby : List(string)
             The data fields to group by. If not specified, a single group
             containing all data objects will be used.
+        **kwds :
+            additional keywords are converted to aggregates using standard
+            shorthand parsing.
 
         Returns
         -------
         self : Chart object
             returns chart to allow for chaining
 
+        Examples
+        --------
+        The aggregate transform allows you to specify transforms directly using
+        the same shorthand syntax as used in encodings:
+
+        >>> import altair as alt
+        >>> chart1 = alt.Chart().transform_aggregate(
+        ...     mean_acc='mean(Acceleration)',
+        ...     groupby=['Origin']
+        ... )
+        >>> print(chart1.transform[0].to_json())
+        {
+          "aggregate": [
+            {
+              "as": "mean_acc",
+              "field": "Acceleration",
+              "op": "mean"
+            }
+          ],
+          "groupby": [
+            "Origin"
+          ]
+        }
+
+        It also supports including AggregatedFieldDef instances or dicts directly,
+        so you can create the above transform like this:
+
+        >>> chart2 = alt.Chart().transform_aggregate(
+        ...     [alt.AggregatedFieldDef(field='Acceleration', op='mean',
+        ...                             **{'as': 'mean_acc'})],
+        ...     groupby=['Origin']
+        ... )
+        >>> chart2.transform == chart1.transform
+        True
+
         See Also
         --------
         alt.AggregateTransform : underlying transform object
         """
-        kwargs['aggregate'] = aggregate
-        kwargs['groupby'] = groupby
-        return self._add_transform(core.AggregateTransform(**kwargs))
-
-    def transform_bin(self, as_=Undefined, bin=Undefined, field=Undefined, **kwargs):
+        if aggregate is Undefined:
+            aggregate = []
+        for key, val in kwds.items():
+            parsed = utils.parse_shorthand(val)
+            dct = {'as': key,
+                   'field': parsed.get('field', Undefined),
+                   'op': parsed.get('aggregate', Undefined)}
+            aggregate.append(core.AggregatedFieldDef(**dct))
+        return self._add_transform(core.AggregateTransform(aggregate=aggregate,
+                                                           groupby=groupby))
+
+    def transform_bin(self, as_=Undefined, field=Undefined, bin=True, **kwargs):
         """
         Add a BinTransform to the schema.
 
@@ -501,6 +551,28 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         self : Chart object
             returns chart to allow for chaining
 
+        Examples
+        --------
+        >>> import altair as alt
+        >>> chart = alt.Chart().transform_bin("x_binned", "x")
+        >>> chart.transform[0]
+        BinTransform({
+          bin: True,
+          field: 'x',
+          as: 'x_binned'
+        })
+
+        >>> chart = alt.Chart().transform_bin("x_binned", "x",
+        ...                                   bin=alt.Bin(maxbins=10))
+        >>> chart.transform[0]
+        BinTransform({
+          bin: BinParams({
+            maxbins: 10
+          }),
+          field: 'x',
+          as: 'x_binned'
+        })
+
         See Also
         --------
         alt.BinTransform : underlying transform object
@@ -524,22 +596,55 @@ class TopLevelMixin(mixins.ConfigMethodMixin):
         calculate : string or alt.expr expression
             An expression string. Use the variable `datum` to refer to the
             current data object.
+        **kwargs
+            transforms can also be passed by keyword argument; see Examples
 
         Returns
         -------
         self : Chart object
             returns chart to allow for chaining
 
+        Examples
+        --------
+        >>> import altair as alt
+        >>> from altair import datum, expr
+
+        >>> chart = alt.Chart().transform_calculate(y = 2 * expr.sin(datum.x))
+        >>> chart.transform[0]
+        CalculateTransform({
+          calculate: (2 * sin(datum.x)),
+          as: 'y'
+        })
+
+        It's also possible to pass the ``CalculateTransform`` arguments directly:
+
+        >>> kwds = {'as': 'y', 'calculate': '2 * sin(datum.x)'}
+        >>> chart = alt.Chart().transform_calculate(**kwds)
+        >>> chart.transform[0]
+        CalculateTransform({
+          calculate: '2 * sin(datum.x)',
+          as: 'y'
+        })
+
+        As the first form is easier to write and understand, that is the
+        recommended method.
+
         See Also
         --------
         alt.CalculateTransform : underlying transform object
         """
-        if as_ is not Undefined:
+        if as_ is Undefined:
+            as_ = kwargs.pop('as', Undefined)
+        else:
             if 'as' in kwargs:
                 raise ValueError("transform_calculate: both 'as_' and 'as' passed as arguments.")
-            kwargs['as'] = as_
-        kwargs['calculate'] = calculate
-        return self._add_transform(core.CalculateTransform(**kwargs))
+        if as_ is not Undefined or calculate is not Undefined:
+            dct = {'as': as_, 'calculate': calculate}
+            self = self._add_transform(core.CalculateTransform(**dct))
+        for as_, calculate in kwargs.items():
+            dct = {'as': as_, 'calculate': calculate}
+            self = self._add_transform(core.CalculateTransform(**dct))
+        return self
 
     def transform_filter(self, filter, **kwargs):
         """
diff --git a/altair/vegalite/v2/examples/choropleth.py b/altair/vegalite/v2/examples/choropleth.py
index 5aeb729a..8efbe16e 100644
--- a/altair/vegalite/v2/examples/choropleth.py
+++ b/altair/vegalite/v2/examples/choropleth.py
@@ -9,7 +9,7 @@ import altair as alt
 from vega_datasets import data
 
 counties = alt.topo_feature(data.us_10m.url,'counties')
-unemp_data = alt.UrlData(data.unemployment.url)
+unemp_data = data.unemployment.url
 
 
 alt.Chart(counties).mark_geoshape().properties(
diff --git a/altair/vegalite/v2/examples/choropleth_repeat.py b/altair/vegalite/v2/examples/choropleth_repeat.py
index ecbe512b..2a6656c5 100644
--- a/altair/vegalite/v2/examples/choropleth_repeat.py
+++ b/altair/vegalite/v2/examples/choropleth_repeat.py
@@ -10,7 +10,7 @@ from vega_datasets import data
 
 states = alt.topo_feature(data.us_10m.url,'states')
 
-pop_eng_hur = alt.UrlData(data.population_engineers_hurricanes.url)
+pop_eng_hur = data.population_engineers_hurricanes.url
 
 variable_list = ['population','engineers','hurricanes']
 
diff --git a/doc/index.rst b/doc/index.rst
index 515e5877..111f4e1d 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -4,7 +4,7 @@ Altair: Declarative Visualization in Python
 .. altair-minigallery::
    :size: 15
    :shuffle:
-   :seed: 6543
+   :seed: 654
 
 Altair is a declarative statistical visualization library for Python, based on
 Vega_ and Vega-Lite_.
diff --git a/doc/user_guide/API.rst b/doc/user_guide/API.rst
index 4b7fc82d..bad28ffc 100644
--- a/doc/user_guide/API.rst
+++ b/doc/user_guide/API.rst
@@ -81,3 +81,10 @@ Other Objects
    :members:
    :imported-members:
    :exclude-members: Chart,LayerChart,FacetChart,
+
+Vega Expressions
+----------------
+
+.. automodule:: altair.expr
+   :members:
+   :imported-members:
diff --git a/doc/user_guide/transform.rst b/doc/user_guide/transform.rst
index 443045a9..36f4ff8a 100644
--- a/doc/user_guide/transform.rst
+++ b/doc/user_guide/transform.rst
@@ -20,8 +20,9 @@ for example, a URL pointer to a JSON or CSV file. It can also be useful in a
 compound chart where different views of the dataset require different
 transformations.
 
-This second approach -- specifying data transformations within the chart itself
--- can be accomplished using the ``transform_*`` methods of top-level objects:
+This second approach -- specifying data transformations within the chart
+specification itself -- can be accomplished using the ``transform_*``
+methods of top-level objects:
 
 =========================================  ================================================================================
 Method                                     Description
@@ -37,33 +38,606 @@ Method                                     Description
 
 We will see some examples of these transforms in the following sections.
 
-.. _user-guide-expressions:
+.. _user-guide-aggregate-transform:
 
-Altair Expressions
+Aggregate Transforms
+~~~~~~~~~~~~~~~~~~~~
+There are two ways to aggregate data within Altair: within the encoding itself,
+or using a top level aggregate transform.
+
+The aggregate property of a field definition can be used to compute aggregate
+summary statistics (e.g., median, min, max) over groups of data.
+
+If at least one fields in the specified encoding channels contain aggregate,
+the resulting visualization will show aggregate data. In this case, all
+fields without aggregation function specified are treated as group-by fields
+in the aggregation process.
+
+For example, the following bar chart aggregates mean of ``acceleration``,
+grouped by the number of Cylinders.
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    cars = data.cars.url
+
+    alt.Chart(cars).mark_bar().encode(
+        y='Cylinders:O',
+        x='mean(Acceleration):Q',
+    )
+
+The Altair shorthand string::
+
+    ...
+    y='mean(acceleration):Q',
+    ...
+
+is made available for convenience, and is equivalent to the longer form::
+
+    ...
+    y=alt.Y(field='acceleration', aggregate='mean', type='quantitative')
+    ...
+
+For more information on shorthand encodings specifications, see
+:ref:`encoding-aggregates`.
+
+The same plot can be shown using an explicitly computed aggregation, using the
+:meth:`~Chart.transform_aggregate` method:
+
+.. altair-plot::
+
+    alt.Chart(cars).mark_bar().encode(
+        y='Cylinders:O',
+        x='mean_acc:Q'
+    ).transform_aggregate(
+        mean_acc='mean(Acceleration)',
+        groupby=["Cylinders"]
+    )
+
+For a list of available aggregates, see :ref:`encoding-aggregates`.
+
+.. _user-guide-bin-transform:
+
+Bin transforms
+~~~~~~~~~~~~~~
+As with :ref:`user-guide-aggregate-transform`, there are two ways to apply
+a bin transform in Altair: within the encoding itself, or using a top-level
+bin transform.
+
+An common application of a bin transform is when creating a histogram:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    movies = data.movies.url
+
+    alt.Chart(movies).mark_bar().encode(
+        alt.X("IMDB_Rating:Q", bin=True),
+        y='count()',
+    )
+
+But a bin transform can be useful in other applications; for example, here we
+bin a continuous field to create a discrete color map:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    cars = data.cars.url
+
+    alt.Chart(cars).mark_point().encode(
+        x='Horsepower:Q',
+        y='Miles_per_Gallon:Q',
+        color=alt.Color('Acceleration:Q', bin=alt.Bin(maxbins=5))
+    )
+
+In the first case we set ``bin = True``, which uses the default bin settings.
+In the second case, we exercise more fine-tuned control over the bin parameters
+by passing a :class:`~altair.Bin` object.
+
+If you are using the same binnings in multiple chart components, it can be useful
+to instead define the binning at the top level, using :meth:`~Chart.transform_bin`
+method.
+
+Here is the above histogram created using a top-level bin transform:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    movies = data.movies.url
+
+    alt.Chart(movies).mark_bar().encode(
+        x='binned_rating:O',
+        y='count()',
+    ).transform_bin(
+        'binned_rating', field='IMDB_Rating'
+    )
+
+And here is the transformed color scale using a top-level bin transform:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    cars = data.cars.url
+
+    alt.Chart(cars).mark_point().encode(
+        x='Horsepower:Q',
+        y='Miles_per_Gallon:Q',
+        color='binned_acc:O'
+    ).transform_bin(
+        'binned_acc', 'Acceleration', bin=alt.Bin(maxbins=5)
+    )
+
+The advantage of the top-level transform is that the same named field can be
+used in multiple places in the chart if desired.
+Note the slight difference in binning behavior between the encoding-based binnings
+(which preserve the range of the bins) and the transform-based binnings (which
+collapse each bin to a single representative value.
+
+.. _user-guide-calculate-transform:
+
+Calculate Transform
+~~~~~~~~~~~~~~~~~~~
+The calculate transform allows the user to define new fields in the dataset
+which are calculated from other fields using an expression syntax.
+
+As a simple example, here we take data with a simple input sequence, and compute
+a some trigonometric quantities:
+
+.. altair-plot::
+
+    import altair as alt
+    import pandas as pd
+
+    data = pd.DataFrame({'t': range(101)})
+
+    alt.Chart(data).mark_line().encode(
+        x='x:Q',
+        y='y:Q',
+        order='t:Q'
+    ).transform_calculate(
+        x='cos(datum.t * PI / 50)',
+        y='sin(datum.t * PI / 25)'
+    )
+
+Each argument within ``transform_calculate`` is a `Vega expression`_ string,
+which is a well-defined set of javascript-style operations that can be used
+to calculate a new field from an existing one.
+
+To streamline building these vega expressions in Python, Altair provides the
+:mod:`altair.expr` module which provides constants and functions to allow
+these expressions to be constructed with Python syntax; for example:
+
+.. altair-plot::
+
+    from altair import expr, datum
+
+    alt.Chart(data).mark_line().encode(
+        x='x:Q',
+        y='y:Q',
+        order='t:Q'
+    ).transform_calculate(
+        x=expr.cos(datum.t * expr.PI / 50),
+        y=expr.sin(datum.t * expr.PI / 25)
+    )
+
+Altair expressions are designed to output valid Vega expressions. The benefit of
+using them is that proper syntax is ensured by the Python interpreter, and tab
+completion of the :mod:`~expr` submodule can be used to explore the
+available functions and constants.
+
+These expressions can also be used when constructing a
+:ref:`user-guide-filter-transform`, as we shall see next.
+
+.. _user-guide-filter-transform:
+
+Filter Transform
+~~~~~~~~~~~~~~~~
+The filter transform removes objects from a data stream based on a provided
+filter expression, selection, or other filter predicate. A filter can be
+added at the top level of a chart using the :meth:`Chart.transform_filter`
+method. The argument to ``transform_filter`` can be one of a number of
+expressions and objects:
+
+1. A `Vega expression`_ expressed as a string or built using the :mod:`~expr` module
+2. A Field predicate, such as :class:`~FieldOneOfPredicate`,
+   :class:`~FieldRangePredicate`, or :class:`~FieldEqualPredicate`
+3. A Selection predicate or object created by :func:`selection`
+4. A Logical operand that combines any of the above
+
+We'll show a brief example of each of these in the following sections
+
+Filter Expression
+^^^^^^^^^^^^^^^^^
+A filter expression uses the `Vega expression`_ language, either specified
+directly as a string, or built using the :mod:`~expr` module.
+This can be useful when, for example, selecting only a subset of data.
+
+For example:
+
+.. altair-plot::
+
+    import altair as alt
+    from altair import datum
+
+    from vega_datasets import data
+    pop = data.population.url
+
+    alt.Chart(pop).mark_area().encode(
+        x='age:O',
+        y='people:Q',
+    ).transform_filter(
+        (datum.year == 2000) & (datum.sex == 1)
+    )
+
+Notice that, like in the :ref:`user-guide-filter-transform`, data values are
+referenced via the name ``datum``.
+
+Field Predicates
+^^^^^^^^^^^^^^^^
+Field predicates overlap somewhat in function with expression predicates, but
+have the advantage that their contents are validated by the schema. Examples
+are:
+
+- :class:`~FieldEqualPredicate` evaluates whether a field is equal to
+  a particular value
+- :class:`~FieldOneOfPredicate` evaluates whether a field is among a list of
+  specified values.
+- :class:`~FieldRangePredicate` evaluates whether a continuous field is within
+  a range of values.
+
+Here is an examaple of a :class:`~FieldEqualPredicate` used to select just the
+values from year 2000 as in the above chart:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+    pop = data.population.url
+
+    alt.Chart(pop).mark_line().encode(
+        x='age:O',
+        y='sum(people):Q',
+        color='year:O'
+    ).transform_filter(
+        alt.FieldEqualPredicate(field='year', equal=2000)
+    )
+
+A :class:`~FieldOneOfPredicate` is similar, but allows selection of any number
+of specific values:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+    pop = data.population.url
+
+    alt.Chart(pop).mark_line().encode(
+        x='age:O',
+        y='sum(people):Q',
+        color='year:O'
+    ).transform_filter(
+        alt.FieldOneOfPredicate(field='year', oneOf=[1900, 1950, 2000])
+    )
+
+Finally, a :meth:`~FieldRangePredicate` allows selecting values within a
+particular continuous range:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+    pop = data.population.url
+
+    alt.Chart(pop).mark_line().encode(
+        x='age:O',
+        y='sum(people):Q',
+        color='year:O'
+    ).transform_filter(
+        alt.FieldRangePredicate(field='year', range=[1960, 2000])
+    )
+
+Selection Predicates
+^^^^^^^^^^^^^^^^^^^^
+Selection predicates can be used to filter data based on a selection. While
+these can be constructed directly using a :class:`~SelectionPredicate` class,
+in Altair it is often more convenient to construct them using the
+:func:`~selection` function. For example, this chart uses a multi-selection
+that allows the user to click or shift-click on the bars in the bottom chart
+to select the data to be shown in the top chart:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+    pop = data.population.url
+
+    selection = alt.selection_multi(fields=['year'])
+
+    top = alt.Chart().mark_line().encode(
+        x='age:O',
+        y='sum(people):Q',
+        color='year:O'
+    ).properties(
+        width=600, height=200
+    ).transform_filter(
+        selection
+    )
+
+    bottom = alt.Chart().mark_bar().encode(
+        x='year:O',
+        y='sum(people):Q',
+        color=alt.condition(selection, alt.value('steelblue'), alt.value('lightgray'))
+    ).properties(
+        width=600, height=100,
+        selection=selection
+    )
+
+    alt.vconcat(
+        top, bottom,
+        data=pop
+    )
+
+Logical Operands
+^^^^^^^^^^^^^^^^
+At times it is useful to combine several types of predicates into a single
+selection. This can be accomplished using the various logical operand classes:
+
+- :class:`~LogicalOrPredicate`
+- :class:`~LogicalAndPredicate`
+- :class:`~LogicalNotPredicate`
+
+These are not yet part of the Altair interface
+(see `Issue 693 <https://github.com/altair-viz/altair/pull/693>`_)
+but can be constructed explicitly; for example, here we plot US population
+distributions for all data *except* the years 1950-1960,
+by applying a ``LogicalNotPredicate`` schema to a ``FieldRangePredicate``:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    pop = data.population.url
+
+    alt.Chart(pop).mark_line().encode(
+        x='age:O',
+        y='sum(people):Q',
+        color='year:O'
+    ).properties(
+        width=600, height=200
+    ).transform_filter(
+        {'not': alt.FieldRangePredicate(field='year', range=[1900, 1950])}
+    )
+
+
+.. _user-guide-lookup-transform:
+
+Lookup Transform
+~~~~~~~~~~~~~~~~
+The lookup transform extends a primary data source by looking up values from
+another data source; it is similar to a one-sided join. A lookup can be added
+at the top level of a chart using the :meth:`Chart.transform_lookup` method.
+
+By way of example, imagine you have two sources of data that you would like
+to combine and plot: one is a list of names of people along with their height
+and weight, and the other is some information about which groups they belong
+to. This example data is available in ``vega_datasets``:
+
+.. altair-plot::
+   :output: none
+
+   from vega_datasets import data
+   people = data.lookup_people()
+   groups = data.lookup_groups()
+
+We know how to visualize each of these datasets separately; for example:
+
+.. altair-plot::
+
+    top = alt.Chart(people).mark_square(size=200).encode(
+        x=alt.X('age:Q', scale=alt.Scale(zero=False)),
+        y=alt.Y('height:Q', scale=alt.Scale(zero=False)),
+        color='name:N',
+        tooltip='name:N'
+    ).properties(
+        width=400, height=200
+    )
+
+    bottom = alt.Chart(groups).mark_rect().encode(
+        x='person:N',
+        y='group:O'
+    ).properties(
+        width=400, height=100
+    )
+
+    alt.vconcat(top, bottom)
+
+If we would like to plot features that reference both datasets (for example, the
+average age within each group), we need to combine the two datasets.
+This can be done either as a data preprocessing step, using tools available
+in Pandas, or as part of the visualization using a :class:`~LookupTransform`
+in Altair.
+
+Combining Datasets with pandas.merge
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Pandas provides a wide range of tools for merging and joining datasets; see
+`Merge, Join, and Concatenate <https://pandas.pydata.org/pandas-docs/stable/merging.html>`_
+for some detailed examples.
+For the above data, we can merge the data and create a combined chart as follows:
+
+.. altair-plot::
+
+    import pandas as pd
+    merged = pd.merge(groups, people, how='left',
+                      left_on='person', right_on='name')
+
+    alt.Chart(merged).mark_bar().encode(
+        x='mean(age):Q',
+        y='group:O'
+    )
+
+We specify a left join, meaning that for each entry of the "person" column in
+the groups, we seek the "name" column in people and add the entry to the data.
+From this, we can easily create a bar chart representing the mean age in each group.
+
+Combining Datasets with a Lookup Transform
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+For some data sources (e.g. data available at a URL, or data that is streaming),
+it is desirable to have a means of joining data without having to download
+it for pre-processing in Pandas.
+This is where Altair's :meth:`~Chart.transform_lookup` comes in.
+To reproduce the above combined plot by combining datasets within the
+chart specification itself, we can do the following:
+
+.. altair-plot::
+
+    alt.Chart(groups).mark_bar().encode(
+        x='mean(age):Q',
+        y='group:O'
+    ).transform_lookup(
+        lookup='person',
+        from_=alt.LookupData(data=people, key='name',
+                             fields=['age', 'height'])
+    )
+
+Here ``lookup`` names the field in the groups dataset on which we will match,
+and the ``from_`` argument specifies a :class:`~LookupData` structure where
+we supply the second dataset, the lookup key, and the fields we would like to
+extract.
+
+Example: Lookup Transforms for Geographical Visualization
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Lookup transforms are often particularly important for geographic visualization,
+where it is common to combine tabular datasets with datasets that specify
+geographic boundaries to be visualized; for example, here is a visualization
+of unemployment rates per county in the US:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    counties = alt.topo_feature(data.us_10m.url, 'counties')
+    unemp_data = data.unemployment.url
+
+    alt.Chart(counties).mark_geoshape().encode(
+        color='rate:Q'
+    ).transform_lookup(
+        lookup='id',
+        from_=alt.LookupData(unemp_data, 'id', ['rate'])
+    ).properties(
+        projection={'type': 'albersUsa'},
+        width=500, height=300
+    )
+
+
+.. _user-guide-timeunit-transform:
+
+TimeUnit Transform
 ~~~~~~~~~~~~~~~~~~
-Transformation expressions in Vega-Lite are specified as strings of javascript
-code, with a well-defined set of functions and constants available
-(see Vega's `Expression Documentation <https://github.com/vega/vega/wiki/Expressions>`_).
-For convenience when when working from Python, Altair provides an API
-in the :mod:`altair.expr` module to allow users to build these expressions
-more idiomatically within Python.
+TimeUnit transforms are used to discretize dates and times within Altair.
+As with the :ref:`user-guide-aggregate-transform` and :ref:`user-guide-bin-transform`
+discussed above, they can be defined either as part of the encoding, or as a
+top-level transform.
+
+These are the available time units:
+
+- ``"year"``, ``"yearquarter"``, ``"yearquartermonth"``, ``"yearmonth"``,
+  ``"yearmonthdate"``, ``"yearmonthdatehours"``, ``"yearmonthdatehoursminutes"``,
+  ``"yearmonthdatehoursseconds"``.
+- ``"quarter"``, ``"quartermonth"``
+- ``"month"``, ``"monthdate"``
+- ``"date"`` (Day of month, i.e., 1 - 31)
+- ``"day"`` (Day of week, i.e., Monday - Friday)
+- ``"hours"``, ``"hoursminutes"``, ``"hoursminutesseconds"``
+- ``"minutes"``, ``"minutesseconds"``
+- ``"seconds"``, ``"secondsmilliseconds"``
+- ``"milliseconds"``
+
+TimeUnit Within Encoding
+^^^^^^^^^^^^^^^^^^^^^^^^
+Any temporal field definition can include a ``timeUnit`` argument to discretize
+the temporal data.
+
+For example, here we plot a dataset that consists of hourly temperature
+measurements in Seattle during the year 2010:
+
+.. altair-plot::
+
+    import altair as alt
+    from vega_datasets import data
+
+    temps = data.seattle_temps.url
+
+    alt.Chart(temps).mark_line().encode(
+        x='date:T',
+        y='temp:Q'
+    )
+
+The plot is too busy due to the amount of data points squeezed into the short
+time; we can make it a bit cleaner by discretizing it, for example, by month
+(``timeUnit="month"``) and plotting only the mean monthly temperature:
+
+.. altair-plot::
+
+    alt.Chart(temps).mark_line().encode(
+        alt.X('date:T', timeUnit='month'),
+        y='mean(temp):Q'
+    )
+
+Notice that by default timeUnit output is a continuous quantity; if you would
+instead like it to be a categorical, you can specify the ordinal type.
+This can be useful when plotting a bar chart or other discrete chart type:
+
+.. altair-plot::
+
+    alt.Chart(temps).mark_bar().encode(
+        alt.X('date:O', timeUnit='month'),
+        y='mean(temp):Q'
+    )
 
-So, for example, if you want to perform the following data filtration:
+Multiple time units can be combined within a single plot to yield interesting
+views of your data; for example, here we extract both the month and the day
+to give a profile of Seattle temperatures through the year:
 
-.. code-block:: python
+.. altair-plot::
 
-   chart.transform_filter("datum.y < sin(datum.t + PI)")
+    alt.Chart(temps).mark_rect().encode(
+        alt.X('date:O', timeUnit='date', axis=alt.Axis(title='day')),
+        alt.Y('date:O', timeUnit='month', axis=alt.Axis(title='month')),
+        color='max(temp):Q'
+    ).properties(
+        title="2010 Daily High Temperatures in Seattle (F)"
+    )
 
-You can instead write the following:
+TimeUnit as a Transform
+^^^^^^^^^^^^^^^^^^^^^^^
+Other times it is convenient to specify a timeUnit as a top-level transform,
+particularly when the value may be reused.
+This can be done most conveniently using the :meth:`Chart.transform_timeunit`
+method. For example:
 
-.. code-block:: python
+.. altair-plot::
 
-   from altair.expr import datum, sin, PI
-   chart.transform_filter(datum.y < sin(datum.t) + PI)
+    alt.Chart(temps).mark_line().encode(
+        alt.X('month:T', axis=alt.Axis(format='%b')),
+        y='mean(temp):Q'
+    ).transform_timeunit(
+        "month", field="date", timeUnit="month"
+    )
 
-This approach has the benefit that syntax errors in the formula will be caught
-in Python rather than in the chart renderer; you can also use Jupyter
-tab-completion on the :mod:`expr` module to see what functions and constants
-are available.
+Notice that because the ``timeUnit`` is not part of the encoding channel here,
+it is often necessary to add an axis formatter to ensure appropriate axis
+labels.
 
-*TODO: section detailing each type of transform*
+.. _Vega expression: https://vega.github.io/vega/docs/expressions/


Test patch:
diff --git a/altair/expr/tests/test_expr.py b/altair/expr/tests/test_expr.py
index 7ad2861c..811aa131 100644
--- a/altair/expr/tests/test_expr.py
+++ b/altair/expr/tests/test_expr.py
@@ -17,7 +17,7 @@ def test_unary_operations():
 
 def test_binary_operations():
     OP_MAP = {'+': operator.add, '-': operator.sub,
-              '*': operator.mul, '/': operator.floordiv,
+              '*': operator.mul, '/': operator.truediv,
               '%': operator.mod, '===': operator.eq,
               '<': operator.lt, '<=': operator.le,
               '>': operator.gt, '>=': operator.ge,
diff --git a/altair/vegalite/v2/tests/test_api.py b/altair/vegalite/v2/tests/test_api.py
index 5e079058..3fb09982 100644
--- a/altair/vegalite/v2/tests/test_api.py
+++ b/altair/vegalite/v2/tests/test_api.py
@@ -240,8 +240,10 @@ def test_SelectionMapping():
 
 def test_transforms():
     # aggregate transform
-    chart = alt.Chart().transform_aggregate([], ['foo'])
-    kwds = {'aggregate': [], 'groupby': ['foo']}
+    agg1 = alt.AggregatedFieldDef(**{'as': 'x1', 'op': 'mean', 'field': 'y'})
+    agg2 = alt.AggregatedFieldDef(**{'as': 'x2', 'op': 'median', 'field': 'z'})
+    chart = alt.Chart().transform_aggregate([agg1], ['foo'], x2='median(z)')
+    kwds = dict(aggregate=[agg1, agg2], groupby=['foo'])
     assert chart.transform == [alt.AggregateTransform(**kwds)]
 
     # bin transform