Repository: bokeh/bokeh

Issue:
Extending/Fixing Crossfilter
I was looking at extending the crossfilter functionality, since I've previously built something similar in Matlab.

When working on the current master branch, I noticed that not everything is working how it should. First I started walking through some of the functions under models and added documentation as I understood more about what they were doing. Then, I started fixing some issues that appear to exist due to some change in the interface for the main bokeh functionality.

I wanted to go ahead and reference [this commit](https://github.com/rothnic/bokeh/commit/e338c26adf9b9456f8cd68c43cd66208881c7464) to see if there is someone already working in this area, and see if extending it would be of interest.
### Use Cases

I think there are two major use cases that might be different apps, but overlap somewhat:
- Tableau-like Dashboard creation (create a plot, create another, piece them together with controls)
- Exploratory tool that provides faceting (seems somewhat incompatible with the first use case)
### Features
- More plot types
- More aesthetics to map columns into (color, size, shape, etc)
- "Saving" plots to use in a larger dashboard
- Gracefully handle more columns (big list to scroll through, maybe collapse summary by default)
- Handling multiple data sets (configured to different plots on a dashboard, or combining through simple joins of some kind)



Discussion:
It is nice to see you are working in fixes and documentation of this example... I encourage you to keep going on this one and submit PRs against master as soon as you think you have something consistent to be reviewed... so far, the things I have seen in your branch are really helpful, so...

>  see if extending it would be of interest

For sure...

Yeah, agree with @damianavila thoughts.

@rothnic  On [the linked commit](https://github.com/rothnic/bokeh/commit/e401932ea09e9130461bc5a3097cd4a3c2fbd51e) I see you are using `charts.Scatter`. Have you got something working already? The reason I'm asking is that `charts` implements a very different api from plotting and we have open discussions on how to make it evolve and integrate better with other `bokeh` parts. 

When I opened this up in Pycharm I immediately saw some things that were missing. The scatter plot being one of them. I think I looked around and saw some of the refactoring that had occurred.

The crossfilter app appeared to be directly creating a figure, then using a scatter method. Line was the same. Most of the stuff I have fixed already was with the bar charts. There were some things that were just not working due to changes in other bokeh components or pandas.

The place that I ended up stopping was when I found an issue with how scatter was being used. You will see the issue I'm running into in the last image in [this album](http://imgur.com/a/yvmsG). Also included in this album are just some other images of the state of it as is. 

The last image shows that when the x axis is discrete/categorical the scatter plot isn't showing up. I'd expect to see the scatter plot handle plotting the scatter points aligned on the x axis to discrete values, then the y axis would just be the y values. You'd likely want to use a different kind of plot in the case, but it should work.

@fpliger to answer your question, if I remember correctly that isn't being used. I saw the missing reference to the lowercase "scatter", so naively replaced it with charts.Scatter. I'll look more into the newer charts components.

_Any_ help extending and improving the crossfilter would be extremely appreciated. It is an important priority, but there are currently still lots of important priorities. :) Especially though, if you have experience using and designing these kinds of tools, that would be incredibly valuable!

For what concerns including any `chart` IMHO it'd be better to wait for #1574 because those would just behave like plots and should _just work_. Right know it'd require some machinery to make them work...

The way the tool worked that I mentioned I had worked on, was by abstracting the plot from the interface. If you think of something like ggplot, they have a consistent interface to the user, so a gui wrapper around it would be easy to construct. 

I was taking a similar concept and had what I called views as a subclass of a parent data view. Each view must expose its requirements (aes) to the interface, where, each requirement would map to a column. I mocked the core of this up one day and the rest of the work has been on a closed network, so I still need to update this code, but here is a [histogram example wrapping a legacy function](https://github.com/rothnic/mAware/blob/master/plugins/%40histogram/histogram.m) and [a simple scatter example](https://github.com/rothnic/mAware/blob/master/aware/data_views/%40scatter_view/scatter_view.m). 

The constants were there so the interface could know these things about the class before instantiating one, which makes the interface mostly independent of the plots. Each plot would have some aesthetics that would be mapped to columns, then it may have toggle-able, and/or drop down options (not in example code). By exposing this common information, the tool could provide the UI utilities without having to write any code. In the Matlab world, people tend to not think about the user experience, so you have many legacy tools that all require some special manipulation of the data. I was using the views as a wrapper, to adapt between the two paradigms.

I am interested in these higher level chart types, since my intent is to have an easy to use gui for exploring tabular datasets. To start, I plan on fixing it in its current form to learn more about how it is implemented, then am going to look into extending it afterwards.

@rothnic that sounds like a great plan, let  us know any questions of help we can provide. 

@fpliger I don't agree that the `charts` interface is appropriate for the implementation of the cross filter. `charts` is simplified and schematized with the intent of being very simple and easy for a certain class of users to get the 95% use cases with next to no code. But for a developer, this kind of simplicity is not really the most important thing, and when implementing a complex tool, I think it often makes more sense to use a lower level interface that provides finer control. 


Resolving patch:
diff --git a/bokeh/crossfilter/models.py b/bokeh/crossfilter/models.py
--- a/bokeh/crossfilter/models.py
+++ b/bokeh/crossfilter/models.py
@@ -3,10 +3,11 @@
 import six
 import pandas as pd
 import numpy as np
+
 from ..plotting import curdoc
 from ..models import ColumnDataSource, Range1d, FactorRange, GridPlot
 from ..models.widgets import Select, MultiSelect, InputWidget
-#crossfilter plotting utilities
+# crossfilter plotting utilities
 from .plotting import (make_histogram_source,
                        make_histogram, make_continuous_bar_source,
                        make_categorical_bar_source,
@@ -18,8 +19,20 @@
 
 logger = logging.getLogger(__name__)
 
+
 class DiscreteFacet(object):
+    """Pairing of a field and a unique value, representing a subset of the
+    total data."""
+
     def __init__(self, field, value, label=None):
+        """Sets object properties and creates label if not provided.
+
+        Args:
+          field (str): name of the column
+          value: unique value defined for the column
+          label (str, optional): string representation of the value
+
+        """
         if label is None:
             label = str(value)
         self.field = field
@@ -27,24 +40,61 @@ def __init__(self, field, value, label=None):
         self._value = value
 
     def __repr__(self):
-        return "%s:%s" % (self.field, self.label)
+        return "%s:%s"%(self.field, self.label)
 
     def filter(self, df):
+        """Filters the provided DataFrame to the subset corresponding to value.
+
+        Args:
+          df (DataFrame): contains a column of ``field``
+
+        Returns:
+          DataFrame: filtered to rows, where column ``field`` has values
+            equal to ``_value``.
+
+        """
         return df[df[self.field] == self._value]
 
+
 class ContinuousFacet(DiscreteFacet):
+    """Represents a range of values for a field in a DataFrame."""
+
     def __init__(self, field, value, bins, label=None):
+        """Calls parent ``DiscreteFacet`` and stores bins for later filtering.
+
+        Args:
+          field (str): name of the column
+          value (str): center of range of values in the column
+          bins (list[float]): start and inclusive stop value for the bin
+          label (str, optional): string representation
+
+        """
         super(ContinuousFacet, self).__init__(field, value, label=label)
         self.bins = bins
 
     def filter(self, df):
+        """Filters the provided DataFrame to the subset corresponding to bins.
+
+        Args:
+          df (DataFrame): contains a column of ``field``
+
+        Returns:
+          DataFrame: filtered to rows, where column ``field`` has values
+            within the bounds of ``bins``.
+
+        """
         if self.bins[0] is not None:
             df = df[df[self.field] > self.bins[0]]
         if self.bins[1] is not None:
             df = df[df[self.field] <= self.bins[1]]
         return df
 
+
 class CrossFilter(PlotObject):
+    """Interactive filtering and faceting application with multiple plot
+    types"""
+
+    # identify properties for the data
     columns = List(Dict(String, Any))
     data = Instance(ColumnDataSource)
     filtered_data = Instance(ColumnDataSource)
@@ -58,7 +108,8 @@ class CrossFilter(PlotObject):
     # dict of column name to filtering widgets
     filter_widgets = Dict(String, Instance(PlotObject))
 
-    # dict which aggregates all the selections from the different filtering widgets
+    # dict which aggregates all the selections from the different filtering
+    # widgets
     filtered_selections = Dict(String, Dict(String, Any))
 
     # list of facet vars
@@ -66,31 +117,69 @@ class CrossFilter(PlotObject):
     facet_y = List(String, default=[])
     facet_tab = List(String, default=[])
 
+    # the displayed plot object
+    plot = Instance(PlotObject)
+
+    # configuration properties for the plot
     plot_type = Enum("line", "scatter", "bar")
     x = String
     y = String
     agg = String
     color = String
-    plot = Instance(PlotObject)
+    height = Int()
+    width = Int()
+
+    # identify the selector/drop-down properties
     plot_selector = Instance(Select)
     x_selector = Instance(Select)
     y_selector = Instance(Select)
     agg_selector = Instance(Select)
-    height = Int()
-    width = Int()
+
     def __init__(self, *args, **kwargs):
+        """Creates original and filtered ColumnDataSource and handles defaults.
+
+        The df and starting configuration are only provided the first time
+        init is called, within the create method.
+
+        Kwargs:
+          df (DataFrame): the data to use in the crossfilter app
+          plot_type (str, optional): starting plot type
+          agg (str, optional): starting aggregation type
+
+        """
         if 'df' in kwargs:
             self._df = kwargs.pop('df')
             kwargs['data'] = ColumnDataSource(data=self.df)
             kwargs['filtered_data'] = ColumnDataSource(data=self.df)
+
+        # default plot type
         if 'plot_type' not in kwargs:
             kwargs['plot_type'] = "scatter"
+
+        # default aggregation type
         if 'agg' not in kwargs:
             kwargs['agg'] = 'sum'
-        super(CrossFilter, self).__init__(*args, **kwargs)
+
+        super(CrossFilter, self).__init__(**kwargs)
 
     @classmethod
     def create(cls, **kwargs):
+        """Performs all one-time construction of bokeh objects.
+
+        This classmethod is required due to the way that bokeh handles the
+        python and javascript components. The initialize method will be
+        called each additional time the app is updated (including once in
+        the create method), but the PlotObject infrastructure will find that
+        the object already exists in any future calls, and will not create a
+        new object.
+
+        Kwargs:
+          df (DataFrame): the data to use in the crossfilter app
+          plot_type (str, optional): starting plot type
+          agg (str, optional): starting aggregation type
+
+        :return:
+        """
         obj = cls(**kwargs)
         obj.set_metadata()
         choices = obj.make_plot_choices()
@@ -100,6 +189,8 @@ def create(cls, **kwargs):
         return obj
 
     def set_input_selector(self):
+        """Creates and configures each selector (drop-down menu)."""
+
         col_names = [x['name'] for x in self.columns]
 
         self.plot_selector = Select.create(
@@ -128,84 +219,174 @@ def set_input_selector(self):
         )
 
     def update_plot_choices(self, input_dict):
-        for k,v in input_dict.items():
+        """Sets object attributes corresponding to input_dict's values.
+
+        Args:
+          input_dict (dict): dict with x, y, and plot_type keys
+
+        """
+        for k, v in input_dict.items():
             if getattr(self, k) is None:
                 setattr(self, k, v)
 
     def column_descriptor_dict(self):
+        """Creates column stats dict with keys of column names.
+
+        Returns:
+          dict: dict with key per column in data, where values are column stats
+
+        """
         column_descriptors = {}
         for x in self.columns:
             column_descriptors[x['name']] = x
         return column_descriptors
 
     def continuous_columns(self):
+        """Returns list of column descriptors for the non-Discrete columns.
+
+        Returns:
+          list(dict): list of dicts, containing metadata about columns
+
+        """
         return [x for x in self.columns if x['type'] != 'DiscreteColumn']
 
     def discrete_columns(self):
+        """Returns list of column descriptors for the Discrete columns.
+
+        Returns:
+          list(dict): list of dicts, containing metadata about columns
+
+        """
         return [x for x in self.columns if x['type'] == 'DiscreteColumn']
 
     def make_plot_choices(self):
+        """Selects first two continuous columns for x,y during initial setup
+
+        Returns:
+          dict: x, y, and plot_type keys and values for initial setup
+
+        """
         x, y = [x['name'] for x in self.continuous_columns()[:2]]
-        return {'x' : x, 'y' : y, 'plot_type' : scatter}
+        return {'x': x, 'y': y, 'plot_type': 'scatter'}
 
     def set_plot(self):
-        if self.x == self.y:
-            return
+        """Makes and sets the plot based on the current configuration of app."""
+
         plot = self.make_plot()
         self.plot = plot
         curdoc()._add_all()
 
     def make_plot(self):
-        if all([len(self.facet_x) ==0,
+        """Makes the correct plot layout type, based on app's current config."""
+
+        # no faceting
+        if all([len(self.facet_x) == 0,
                 len(self.facet_y) == 0,
                 len(self.facet_tab) == 0]):
             return self.make_single_plot()
 
-        if all([len(self.facet_x) !=0,
-                len(self.facet_y) == 0,
+        # x xor y faceting
+        if all([(len(self.facet_x) != 0) ^ (len(self.facet_y) != 0),
                 len(self.facet_tab) == 0]):
-            return self.make_all_facet_plot()
+            return self.make_1d_facet_plot()
 
-        if all([len(self.facet_x) !=0,
+        # x and y faceting
+        if all([len(self.facet_x) != 0,
                 len(self.facet_y) != 0,
                 len(self.facet_tab) == 0]):
-            return self.make_xy_facet_plot()
+            return self.make_2d_facet_plot()
 
     def make_facets(self, dimension):
+        """Creates combination of all facets for the provided dimension
+
+        Args:
+          dimension (str): name of the dimension to create facets for
+
+        Returns:
+          list(list(DiscreteFacet or ContinuousFacet)): list of list of
+            unique facet combinations
+
+        """
         if dimension == 'x':
             facets = self.facet_x
         else:
             facets = self.facet_y
+
+        # create facets for each column
         column_descriptor_dict = self.column_descriptor_dict()
-        start = [[]]
+        all_facets = [[]]
         for field in facets:
+
+            # create facets from discrete columns
             if column_descriptor_dict[field]['type'] == 'DiscreteColumn':
-                facets = [DiscreteFacet(field, val) for val in self.df[field].unique()]
-                start = cross(start, facets)
+                field_facets = [DiscreteFacet(field, val) for val in
+                          np.unique(self.df[field].values)]
+
+                # combine any facets as required
+                all_facets = cross(all_facets, field_facets)
             else:
+                # create quantile based discrete data and pairs of bins
                 categorical, bins = pd.qcut(self.df[field], 4, retbins=True)
-                values = categorical.levels
-                bins = [[bins[idx], bins[idx+1]] for idx in range(len(bins) - 1)]
+                cats = categorical.cat.categories
+                bins = [[bins[idx], bins[idx + 1]] for idx in range(len(bins)- 1)]
                 bins[0][0] = None
-                facets = [ContinuousFacet(field, value, bin) for bin, value in zip(bins, values)]
-                start = cross(start, facets)
 
-        return start
+                # create list of facets
+                field_facets = [ContinuousFacet(field, value, bin) for
+                                bin, value in zip(bins, cats)]
 
-    def facet_title(self, facets):
+                # combine any facets as required
+                all_facets = cross(all_facets, field_facets)
+
+        return all_facets
+
+    @staticmethod
+    def facet_title(facets):
+        """Joins list of facets by commas.
+
+        Args:
+          facets (list(DiscreteFacet or ContinuousFacet)): list of facets,
+          which are a combination of column and unique value within it
+
+        Returns:
+          str: string representation of the combination of facets
+
+        """
         title = ",".join([str(x) for x in facets])
         return title
 
     def facet_data(self, facets, df=None):
+        """Filters data to the rows associated with the given facet.
+
+        Args:
+          facets (list(DiscreteFacet or ContinuousFacet)): list of facets,
+          which are a combination of column and unique value within it
+          df (DataFrame, optional): data to be filtered on
+
+        Returns:
+          DataFrame: filtered DataFrame based on provided facets
+
+        """
         if df is None:
             df = self.filtered_df
         for f in facets:
             df = f.filter(df)
         return df
 
-    def make_all_facet_plot(self):
-        all_facets = self.make_facets('x')
+    def make_1d_facet_plot(self):
+        """Creates the faceted plots when a facet is added to the x axis.
+
+        Returns:
+          GridPlot: a grid of plots, where each plot has subset of data
+
+        """
+        if self.facet_x:
+            all_facets = self.make_facets('x')
+        else:
+            all_facets = self.make_facets('y')
         plots = []
+
+        # loop over facets and create single plots for data subset
         for facets in all_facets:
             title = self.facet_title(facets)
             df = self.facet_data(facets, self.filtered_df)
@@ -214,20 +395,40 @@ def make_all_facet_plot(self):
                 tools="pan,wheel_zoom"
             )
             plot.min_border = 0
+
+            # append single plot to list of plots
             plots.append(plot)
+
+        # create squarish grid based on number of plots
         chunk_size = int(np.ceil(np.sqrt(len(plots))))
+
+        # create list of lists of plots, where each list of plots is a row
         grid_plots = []
         for i in range(0, len(plots), chunk_size):
-            chunk =  plots[i:i+chunk_size]
+            chunk = plots[i:i + chunk_size]
             grid_plots.append(chunk)
-        grid = GridPlot(children=grid_plots, plot_width=200 * chunk_size)
+
+        # return the grid as the plot
+        grid = GridPlot(children=grid_plots, plot_width=200*chunk_size)
         return grid
 
-    def make_xy_facet_plot(self):
+    def make_2d_facet_plot(self):
+        """Creates the grid of plots when there are both x and y facets.
+
+        Returns:
+          GridPlot: grid of x and y facet combinations
+
+        """
+
+        # ToDo: gracefully handle large combinations of facets
         all_facets_x = self.make_facets('x')
         all_facets_y = self.make_facets('y')
         grid_plots = []
+
+        # y faceting down column
         for facets_y in all_facets_y:
+
+            # x faceting across row
             row = []
             for facets_x in all_facets_x:
                 facets = facets_x + facets_y
@@ -238,83 +439,208 @@ def make_xy_facet_plot(self):
                     tools="pan,wheel_zoom"
                 )
                 plot.min_border = 0
-                plot.border_symmetry = "none"
+                plot.v_symmetry = False
+                plot.h_symmetry = False
                 row.append(plot)
+
+            # append the row to the list of rows
             grid_plots.append(row)
-        grid = GridPlot(children=grid_plots, plot_width=200 * len(all_facets_x))
-        return grid
 
+        # return the grid of plots as the plot
+        grid = GridPlot(children=grid_plots, plot_width=200*len(all_facets_x))
+        return grid
 
     def make_single_plot(self, df=None, title=None,
                          plot_width=700, plot_height=680,
-                         tools="pan,wheel_zoom,box_zoom,save,resize,select,reset"
-                     ):
+                         tools="pan,wheel_zoom,box_zoom,save,resize,"
+                               "box_select,reset"):
+        """Creates a plot based on the current app configuration.
+
+        Args:
+          df (DataFrame, optional): data to use for the plot
+          title (str, optional): plot title
+          plot_width (float, optional): width of plot in pixels
+          plot_height (float, optional): height of plot in pixels
+          tools (str, optional): comma separated string of tool names
+
+        Returns:
+          PlotObject: the generated plot
+
+        """
         column_descriptor_dict = self.column_descriptor_dict()
-        if title is None:
-            title ="%s by %s" % (self.x, self.y)
+        do_plot = True
+        faceting = False
+
+        if df is None:
+            source = self.filtered_data
+
+            if len(source.data[self.x]) == 0 or len(source.data[self.y]) == 0:
+                do_plot = False
+                title = 'All data is filtered out'
+        else:
+            source = ColumnDataSource(data=df)
+            faceting = True
 
+        # scatter plot
         if self.plot_type == "scatter":
-            if df is None:
-                source = self.filtered_data
-            else:
-                source = ColumnDataSource(data=df)
+            if title is None:
+                title = "%s by %s"%(self.x, self.y)
+
+            x_range, y_range = self.get_xy_ranges(source)
+
+            # create the figure and configure the source fields to scatter plot
             plot = figure(title_text_font_size="12pt",
                           plot_height=plot_height,
                           plot_width=plot_width,
                           tools=tools,
-                          title=title)
-            plot.scatter(self.x, self.y, source=source)
-            return plot
+                          title=title,
+                          x_range=x_range,
+                          y_range=y_range)
+
+            if do_plot:
+                plot.scatter(self.x, self.y, source=source)
+
+        # line plot
         elif self.plot_type == "line":
-            if df is None:
-                source = self.filtered_data
-            else:
-                source = ColumnDataSource(data=df)
+            if title is None:
+                title = "%s by %s"%(self.x, self.y)
+
+            x_range, y_range = self.get_xy_ranges(source)
+
             plot = figure(title_text_font_size="12pt",
                           plot_height=plot_height,
                           plot_width=plot_width,
                           tools=tools,
-                          title=title)
-            plot.line(self.x, self.y, source=source)
-            return plot
+                          title=title,
+                          x_range=x_range,
+                          y_range=y_range)
+
+            if do_plot:
+                plot.line(self.x, self.y, source=source)
+
+        # bar plot
         elif self.plot_type == 'bar':
+
+            if title is None:
+                title = "%s(%s) by %s"%(self.agg, self.y, self.x)
+
             if df is None:
                 df = self.filtered_df
-            if column_descriptor_dict[self.x]['type'] != 'DiscreteColumn':
-                source = make_continuous_bar_source(
-                    df, self.x, self.y, self.agg)
-                bar_width = 0.7
-                x_range = [df[self.x].min() - bar_width, df[self.x].max() - bar_width]
+
+            # detect all cases that we don't support for this chart
+            if df.empty:
+                do_plot = False
+
+                if not faceting:
+                    title = 'All data is filtered out'
+
+            if self.x == self.y:
+                do_plot = False
+                title = 'Bar does not support two of same column'
+
+            if column_descriptor_dict[self.y]['type'] == 'DiscreteColumn':
+                do_plot = False
+                title = 'Bar does not support discrete y column'
+
+            # plot if we can, otherwise return an empty figure
+            if do_plot:
+
+                # discrete data on x axis
+                if column_descriptor_dict[self.x]['type'] != 'DiscreteColumn':
+                    source = make_continuous_bar_source(
+                        df, self.x, self.y, self.agg)
+                    bar_width = 0.7
+                    x_range = [df[self.x].min() - bar_width,
+                               df[self.x].max() - bar_width]
+
+                # continuous/other data on x axis
+                else:
+                    source = make_categorical_bar_source(df, self.x, self.y,
+                                                         self.agg)
+                    x_range = FactorRange(factors=source.data[self.x])
+
                 plot = make_bar_plot(source, counts_name=self.y,
                                      centers_name=self.x,
-                                     bar_width=bar_width,
                                      plot_height=plot_height,
                                      plot_width=plot_width,
                                      tools=tools,
                                      x_range=x_range)
-                return plot
+                plot.title = title
+
             else:
-                source = make_categorical_bar_source(
-                    df, self.x, self.y, self.agg
-                    )
-                x_range = FactorRange(source[self.x])
-                plot = make_bar_plot(source, counts_name=self.y,
-                                     centers_name=self.x,
-                                     plot_height=plot_height,
-                                     plot_width=plot_width,
-                                     tools=tools,
-                                     x_range=x_range)
-                return plot
+                plot = figure(title_text_font_size="12pt",
+                              plot_height=plot_height,
+                              plot_width=plot_width,
+                              tools=tools,
+                              title=title)
+
+        # the plot requested is not known
+        else:
+            raise NotImplementedError("The chosen plot is not defined")
+
+        return plot
+
+    def get_xy_ranges(self, source):
+        """Provides input required for x_range, y_range when creating a figure.
+
+        Args:
+          source (ColumnDataSource): the source to return correct range for
+
+        Returns:
+          (FactorRange or None, FactorRange or None): returns both x and y
+            ranges for the selected x and y columns. Only if the column is
+            discrete do we need to create a range and later pass that to figure.
+
+        """
+        column_descriptor_dict = self.column_descriptor_dict()
+
+        # detect discrete columns and return a FactorRange if required
+        if column_descriptor_dict[self.x]['type'] == 'DiscreteColumn':
+            x_range = FactorRange(factors=sorted(set(source.data[self.x])))
+        else:
+            x_range = None
+
+        if column_descriptor_dict[self.y]['type'] == 'DiscreteColumn':
+            y_range = FactorRange(factors=sorted(set(source.data[self.y])))
+        else:
+            y_range = None
+
+        return x_range, y_range
 
     def plot_attribute_change(self, obj, attrname, old, new):
+        """Updates app's attribute and plot when view configuration changes.
+
+        Args:
+          obj (Widget): the object that has an attribute change
+          attrname (str): name of the attribute
+          old (type): the previous value of unknown type
+          new (type): the new value of unknown type
+
+        """
+        # ToDo: why pass attribute name? 'value' is passed instead of 'name'
         setattr(self, obj.name, new)
         self.set_plot()
 
     def facet_change(self, obj, attrname, old, new):
+        """Updates plot when any facet configuration changes.
+
+        Args:
+          obj (Widget): the object that has an attribute change
+          attrname (str): name of the attribute
+          old (type): the previous value of unknown type
+          new (type): the new value of unknown type
+
+        """
         self.set_plot()
 
     @property
     def df(self):
+        """The core data that is used by the app for plotting.
+
+        Returns:
+          DataFrame: the original data structure
+
+        """
         if hasattr(self, '_df'):
             return self._df
         else:
@@ -323,6 +649,13 @@ def df(self):
 
     @property
     def filtered_df(self):
+        """The subset of the data to use for plotting.
+
+        Returns:
+          DataFrame: the original data structure
+
+        """
+        # ToDo: shouldn't this just return self.df if there are no filters
         if hasattr(self, '_df'):
             return self._df
         else:
@@ -330,16 +663,27 @@ def filtered_df(self):
                 return self.filtered_data.to_df()
 
     def update(self, **kwargs):
+        # ToDo: determine when this is called
         super(CrossFilter, self).update(**kwargs)
         self.setup_events()
 
     def setup_events(self):
+        """Registers events each time the app changes state."""
+        # ToDo: do we need to re-register events for everything this often
+
+        # watch the app's filtering_columns attribute to setup filters
         self.on_change('filtering_columns', self, 'setup_filter_widgets')
+
+        # register any available filter widget
         for obj in self.filter_widgets.values():
             if isinstance(obj, InputWidget):
                 obj.on_change('value', self, 'handle_filter_selection')
+
+        # watch app column data source attribute for changes
         for obj in self.filter_sources.values():
             obj.on_change('selected', self, 'handle_filter_selection')
+
+        # selector event registration
         if self.plot_selector:
             self.plot_selector.on_change('value', self, 'plot_attribute_change')
         if self.x_selector:
@@ -349,15 +693,29 @@ def setup_events(self):
         if self.agg_selector:
             self.agg_selector.on_change('value', self, 'plot_attribute_change')
 
+        # register to watch the app's facet attributes
         self.on_change('facet_x', self, 'facet_change')
         self.on_change('facet_y', self, 'facet_change')
 
     def handle_filter_selection(self, obj, attrname, old, new):
+        """Filters the data source whenever a filter widget changes.
+
+        Args:
+          obj (Widget): the object that has an attribute change
+          attrname (str): name of the attribute
+          old (type): the previous value of unknown type
+          new (type): the new value of unknown type
+
+        """
         df = self.df
+
+        # loop over the column metadata
         for descriptor in self.columns:
             colname = descriptor['name']
+
+            # handle discrete selections
             if descriptor['type'] == 'DiscreteColumn' and \
-               colname in self.filter_widgets:
+                            colname in self.filter_widgets:
                 selected = self.filter_widgets[colname].value
                 if not selected:
                     continue
@@ -365,10 +723,13 @@ def handle_filter_selection(self, obj, attrname, old, new):
                     df = df[colname == selected]
                 else:
                     df = df[np.in1d(df[colname], selected)]
+
+            # handle time or continuous selections
             elif descriptor['type'] in ('TimeColumn', 'ContinuousColumn') and \
-                colname in self.filter_widgets:
+                            colname in self.filter_widgets:
                 obj = self.filter_sources[colname]
-                #hack because we don't have true range selection
+
+                # hack because we don't have true range selection
                 if not obj.selected:
                     continue
                 min_idx = np.min(obj.selected)
@@ -377,81 +738,125 @@ def handle_filter_selection(self, obj, attrname, old, new):
                 min_val = obj.data['centers'][min_idx]
                 max_val = obj.data['centers'][max_idx]
                 df = df[(df[colname] >= min_val) & (df[colname] <= max_val)]
+
+        # update filtered data and force plot update
         for colname in self.data.column_names:
             self.filtered_data.data[colname] = df[colname]
             self.filtered_data._dirty = True
         self.set_plot()
 
     def clear_selections(self, obj, attrname, old, new):
+        """Updates filter widgets and sources as they are removed.
+
+        Args:
+          obj (Widget): the object that has an attribute change
+          attrname (str): name of the attribute
+          old (type): the previous value of unknown type
+          new (type): the new value of unknown type
+
+        """
         diff = set(old) - set(new)
         column_descriptor_dict = self.column_descriptor_dict()
+
+        # delete any removed filter widgets
         if len(diff) > 0:
             for col in diff:
                 metadata = column_descriptor_dict[col]
                 if metadata['type'] != 'DiscreteColumn':
                     del self.filter_sources[col]
                 del self.filter_widgets[col]
+
+        # update the data based on latest changes
         if diff:
             self.handle_filter_selection(obj, attrname, old, new)
 
 
     def setup_filter_widgets(self, obj, attrname, old, new):
+        """Creates new filter widget each time a new column is added to filters.
+
+        Args:
+          obj (Widget): the object that has an attribute change
+          attrname (str): name of the attribute
+          old (type): the previous value of unknown type
+          new (type): the new value of unknown type
+
+        """
         self.clear_selections(obj, attrname, old, new)
+
+        # add new widget as required for each column set to filter on
         column_descriptor_dict = self.column_descriptor_dict()
         for col in self.filtering_columns:
+
             metadata = column_descriptor_dict[col]
             if not col in self.filter_widgets:
+
+                # discrete
                 if metadata['type'] == 'DiscreteColumn':
                     select = MultiSelect.create(
                         name=col,
                         options=self.df[col].unique().tolist())
                     self.filter_widgets[col] = select
+
+                # continuous
                 else:
                     source = make_histogram_source(self.df[col])
                     self.filter_sources[col] = source
                     hist_plot = make_histogram(self.filter_sources[col],
                                                plot_width=200, plot_height=100,
                                                title_text_font_size='8pt',
-                                               tools='select'
+                                               tools='box_select'
                     )
                     hist_plot.title = col
                     self.filter_widgets[col] = hist_plot
+
         curdoc()._add_all()
 
     def set_metadata(self):
+        """Creates a list of dicts, containing summary info for each column.
+
+        The descriptions are stored in the ``columns`` property.
+
+        """
         descriptors = []
 
         columns = self.df.columns
-
         for c in columns:
+
+            # get description for column from pandas DataFrame
             desc = self.df[c].describe()
+
+            # DiscreteColumn
             if self.df[c].dtype == object:
                 descriptors.append({
-                    'type' : "DiscreteColumn",
-                    'name' : c,
-                    'count' : desc['count'],
-                    'unique' : desc['unique'],
-                    'top' : desc['top'],
-                    'freq' : desc['freq'],
+                    'type': "DiscreteColumn",
+                    'name': c,
+                    'count': desc['count'],
+                    'unique': desc['unique'],
+                    'top': desc['top'],
+                    'freq': desc['freq'],
                 })
 
+            # TimeColumn
             elif self.df[c].dtype == np.datetime64:
                 descriptors.append({
-                    'type' : "TimeColumn",
-                    'name' : c,
-                    'count' : desc['count'],
-                    'unique' : desc['unique'],
-                    'first' : desc['first'],
-                    'last' : desc['last'],
+                    'type': "TimeColumn",
+                    'name': c,
+                    'count': desc['count'],
+                    'unique': desc['unique'],
+                    'first': desc['first'],
+                    'last': desc['last'],
                 })
+
+            # ContinuousColumn
             else:
                 descriptors.append({
-                    'type' : "ContinuousColumn",
-                    'name' : c,
-                    'count' : desc['count'],
-                    'mean' : "%.2f" % desc['mean'],
-                    'std' : "%.2f" % desc['std'],
-                    'min' : "%.2f" % desc['min'],
-                    'max' : "%.2f" % desc['max'],
+                    'type': "ContinuousColumn",
+                    'name': c,
+                    'count': desc['count'],
+                    'mean': "%.2f"%desc['mean'],
+                    'std': "%.2f"%desc['std'],
+                    'min': "%.2f"%desc['min'],
+                    'max': "%.2f"%desc['max'],
                 })
+
         self.columns = descriptors
diff --git a/bokeh/crossfilter/plotting.py b/bokeh/crossfilter/plotting.py
--- a/bokeh/crossfilter/plotting.py
+++ b/bokeh/crossfilter/plotting.py
@@ -2,11 +2,23 @@
 import pandas as pd
 
 from bokeh.models import ColumnDataSource
-from ..models import Range1d
 from ..plotting import figure
 from ..plotting_helpers import _get_select_tool
 
+
 def cross(start, facets):
+    """Creates a unique combination of provided facets.
+    A cross product of an initial set of starting facets with a new set of
+    facets.
+
+    Args:
+      start (list): List of lists of facets
+      facets (list): List of facets
+
+    Returns:
+      list: a list of lists of unique combinations of facets
+
+    """
     new = [[facet] for facet in facets]
     result = []
     for x in start:
@@ -14,50 +26,120 @@ def cross(start, facets):
             result.append(x + n)
     return result
 
+
 def make_histogram_source(series):
+    """Creates a ColumnDataSource containing the bins of the input series.
+
+    Args:
+      series (:py:class:`~pandas.Series`): description
+
+    Returns:
+      ColumnDataSource: includes bin centers with count of items in the bins
+
+    """
     counts, bins = np.histogram(series, bins=50)
     centers = pd.rolling_mean(bins, 2)[1:]
 
     return ColumnDataSource(data={'counts': counts, 'centers': centers})
 
+
 def make_continuous_bar_source(df, x_field, y_field, agg):
+    """Makes discrete, then creates representation of the bars to be plotted.
+
+    Args:
+      df (DataFrame): contains the data to be converted to a discrete form
+      x_field (str): the column in df that maps to the x dim of the plot
+      y_field (str):  the column in df that maps to the y dim of the plot
+      agg (str): the type of aggregation to be used
+
+    Returns:
+      ColumnDataSource: aggregated, discrete form of x,y values
+
+    """
+
+    # Generate dataframe required to use the categorical bar source function
     labels, edges = pd.cut(df[x_field], 50, retbins=True, labels=False)
     centers = pd.rolling_mean(edges, 2)[1:]
     labels = centers[labels]
     df[x_field] = labels
 
-    group = df.groupby(x_field)[y_field]
-    aggregate = getattr(group, agg)
-    result = aggregate().reset_index()
+    # After making it discrete, create the categorical bar source
+    return make_categorical_bar_source(df, x_field, y_field, agg)
 
-    return ColumnDataSource(data=result)
 
 def make_categorical_bar_source(df, x_field, y_field, agg):
+    """Creates representation of the bars to be plotted.
+
+    Args:
+      df (DataFrame): contains the data to be converted to a discrete form
+      x_field (str): the column in df that maps to the x dim of the plot
+      y_field (str):  the column in df that maps to the y dim of the plot
+      agg (str): the type of aggregation to be used
+
+    Returns:
+      ColumnDataSource: aggregated, discrete form of x,y values
+
+    """
+
+    # Get the y values after grouping by the x values
     group = df.groupby(x_field)[y_field]
     aggregate = getattr(group, agg)
+
+    # Convert back to a DataFrame on the aggregated data
     result = aggregate().reset_index()
 
     return ColumnDataSource(data=result)
 
+
 def make_factor_source(series):
-    return ColumnDataSource(data={'factors': p.unique(series)})
+    """Generate data source that is based on the unique values in the series.
+
+    Args:
+      series (:py:class:`~pandas.Series`): contains categorical-like data
+
+    Returns:
+      ColumnDataSource: contains the unique values from the series
+
+    """
+    return ColumnDataSource(data={'factors': series.unique()})
+
 
 def make_bar_plot(datasource, counts_name="counts",
                   centers_name="centers",
                   bar_width=0.7,
                   x_range=None,
                   plot_width=500, plot_height=500,
-                  tools="pan,wheel_zoom,box_zoom,save,resize,select,reset",
-                  title_text_font_size="12pt"
-              ):
+                  tools="pan,wheel_zoom,box_zoom,save,resize,box_select,reset",
+                  title_text_font_size="12pt"):
+    """Utility function to set/calculate default parameters of a bar plot.
+
+    Args:
+      datasource (ColumnDataSource): represents bars to plot
+      counts_name (str): column corresponding to height of the bars
+      centers_name (str): column corresponding to the location of the bars
+      bar_width (float): the width of the bars in the bar plot
+      x_range (list): list of two values, the min and max of the x axis range
+      plot_width (float): width of the plot in pixels
+      plot_height (float): height of the plot in pixels
+      tools (str): comma separated tool names to add to the plot
+      title_text_font_size (str): size of the plot title, e.g., '12pt'
+
+    Returns:
+      figure: plot generated from the provided parameters
+
+    """
     top = np.max(datasource.data[counts_name])
 
+    # Create the figure container
     plot = figure(
-      title="", title_text_font_size=title_text_font_size,
-      plot_width=plot_width, plot_height=plot_height,
-      x_range=x_range, y_range=[0, top], tools=tools,
-    )
+        title="", title_text_font_size=title_text_font_size,
+        plot_width=plot_width, plot_height=plot_height,
+        x_range=x_range, y_range=[0, top], tools=tools)
+
+    # Get the bar values
     y = [val/2.0 for val in datasource.data[counts_name]]
+
+    # Generate the bars in the figure
     plot.rect(centers_name, y, bar_width, counts_name, source=datasource)
 
     plot.min_border = 0
@@ -71,15 +153,36 @@ def make_bar_plot(datasource, counts_name="counts",
     return plot
 
 def make_histogram(datasource,
-                  counts_name="counts",
-                  centers_name="centers",
-                  x_range=None,
-                  bar_width=0.7,
-                  plot_width=500,
-                  plot_height=500,
-                  min_border=40,
-                  tools=None,
-                  title_text_font_size="12pt"):
+                   counts_name="counts",
+                   centers_name="centers",
+                   x_range=None,
+                   bar_width=0.7,
+                   plot_width=500,
+                   plot_height=500,
+                   min_border=40,
+                   tools=None,
+                   title_text_font_size="12pt"):
+    """Utility function to create a histogram figure.
+
+    This is used to create the filter widgets for continuous data in
+    CrossFilter.
+
+    Args:
+      datasource (ColumnDataSource): represents bars to plot
+      counts_name (str): column corresponding to height of the bars
+      centers_name (str): column corresponding to the location of the bars
+      x_range (list): list of two values, the min and max of the x axis range
+      bar_width (float): the width of the bars in the bar plot
+      plot_width (float): width of the plot in pixels
+      plot_height (float): height of the plot in pixels
+      min_border (float): minimum border width of figure in pixels
+      tools (str): comma separated tool names to add to the plot
+      title_text_font_size (str): size of the plot title, e.g., '12pt'
+
+    Returns:
+      figure: histogram plot generated from the provided parameters
+
+    """
     start = np.min(datasource.data[centers_name]) - bar_width
     end = np.max(datasource.data[centers_name]) - bar_width
     plot = make_bar_plot(
diff --git a/examples/app/crossfilter/crossfilter_app.py b/examples/app/crossfilter/crossfilter_app.py
--- a/examples/app/crossfilter/crossfilter_app.py
+++ b/examples/app/crossfilter/crossfilter_app.py
@@ -9,6 +9,6 @@
 @object_page("crossfilter")
 def make_object():
     autompg['cyl'] = autompg['cyl'].astype(str)
-    autompg['origin'] = autompg['cyl'].astype(str)
+    autompg['origin'] = autompg['origin'].astype(str)
     app = CrossFilter.create(df=autompg)
     return app


Test patch: