Repository: bokeh/bokeh

Issue:
error when using a pd.DataFrame whose columns is a CategoricalIndex as ColumnDataSource
bokeh 1.0.0

#### Complete, minimal, self-contained example code that reproduces the issue

```
from bokeh.models import ColumnDataSource
import pandas as pd

columns = pd.CategoricalIndex(['a', 'b', 'c', 'd'])
df = pd.DataFrame(columns=columns, data=[list(range(4)), list(range(3, 7))])
ds = ColumnDataSource(df)

```

#### Stack traceback and/or browser JavaScript console output
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-e15a70ca6553> in <module>
      4 columns = pd.CategoricalIndex(['a', 'b', 'c', 'd'])
      5 df = pd.DataFrame(columns=columns, data=[list(range(4)), list(range(3, 7))])
----> 6 ds = ColumnDataSource(df)

/usr/local/lib/python3.6/site-packages/bokeh/models/sources.py in __init__(self, *args, **kw)
    137         if not isinstance(raw_data, dict):
    138             if pd and isinstance(raw_data, pd.DataFrame):
--> 139                 raw_data = self._data_from_df(raw_data)
    140             elif pd and isinstance(raw_data, pd.core.groupby.GroupBy):
    141                 raw_data = self._data_from_groupby(raw_data)

/usr/local/lib/python3.6/site-packages/bokeh/models/sources.py in _data_from_df(df)
    179         else:
    180             _df.index = pd.Index(_df.index.values, name=index_name)
--> 181         _df.reset_index(inplace=True)
    182 
    183         tmp_data = {c: v.values for c, v in _df.iteritems()}

/usr/local/lib/python3.6/site-packages/pandas/core/frame.py in reset_index(self, level, drop, inplace, col_level, col_fill)
   4137                 # to ndarray and maybe infer different dtype
   4138                 level_values = _maybe_casted_values(lev, lab)
-> 4139                 new_obj.insert(0, name, level_values)
   4140 
   4141         new_obj.index = new_index

/usr/local/lib/python3.6/site-packages/pandas/core/frame.py in insert(self, loc, column, value, allow_duplicates)
   3220         value = self._sanitize_column(column, value, broadcast=False)
   3221         self._data.insert(loc, column, value,
-> 3222                           allow_duplicates=allow_duplicates)
   3223 
   3224     def assign(self, **kwargs):

/usr/local/lib/python3.6/site-packages/pandas/core/internals.py in insert(self, loc, item, value, allow_duplicates)
   4342 
   4343         # insert to the axis; this could possibly raise a TypeError
-> 4344         new_axis = self.items.insert(loc, item)
   4345 
   4346         block = make_block(values=value, ndim=self.ndim,

/usr/local/lib/python3.6/site-packages/pandas/core/indexes/category.py in insert(self, loc, item)
    765         code = self.categories.get_indexer([item])
    766         if (code == -1) and not (is_scalar(item) and isna(item)):
--> 767             raise TypeError("cannot insert an item into a CategoricalIndex "
    768                             "that is not already an existing category")
    769 

TypeError: cannot insert an item into a CategoricalIndex that is not already an existing category
#### Screenshots or screencasts of the bug in action



Discussion:
This issue is related to
https://github.com/pandas-dev/pandas/issues/19136

btw, this works on 0.12.6
Ok,
I think a correction could be in function : `_data_from_df(df)` in bokeh.models.source.py
```
if isinstance(df.columns, pd.CategoricalIndex):
            _df.columns = df.columns.tolist()
```
It should have no side effects since pandas Dataframe are converted in python dictionnary

Resolving patch:
diff --git a/bokeh/models/sources.py b/bokeh/models/sources.py
--- a/bokeh/models/sources.py
+++ b/bokeh/models/sources.py
@@ -172,6 +172,9 @@ def _data_from_df(df):
             except TypeError:
                 raise TypeError('Could not flatten MultiIndex columns. '
                                 'use string column names or flatten manually')
+        # Transform columns CategoricalIndex in list
+        if isinstance(df.columns, pd.CategoricalIndex):
+            _df.columns = df.columns.tolist()
         # Flatten index
         index_name = ColumnDataSource._df_index_name(df)
         if index_name == 'index':


Test patch:
diff --git a/bokeh/models/tests/test_sources.py b/bokeh/models/tests/test_sources.py
--- a/bokeh/models/tests/test_sources.py
+++ b/bokeh/models/tests/test_sources.py
@@ -65,6 +65,19 @@ def test_init_dataframe_index_named_column(self, pd):
         assert [0, 1] == list(ds.data['level_0'])
         assert set(ds.column_names) - set(df.columns) == set(["level_0"])
 
+    def test_init_dataframe_column_categoricalindex(self, pd):
+        columns = pd.CategoricalIndex(['a', 'b'])
+        data = [[0,2], [1,3]]
+        df = pd.DataFrame(columns=columns, data=data)
+        ds = ColumnDataSource(data=df)
+        assert set(df.columns).issubset(set(ds.column_names))
+        for key in columns:
+            assert isinstance(ds.data[key], np.ndarray)
+            assert list(df[key]) == list(ds.data[key])
+        assert isinstance(ds.data['index'], np.ndarray)
+        assert [0, 1] == list(ds.data['index'])
+        assert set(ds.column_names) - set(df.columns) == set(["index"])
+
     def test_init_dataframe_nonstring_named_column(self, pd):
         data = {1: [1, 2], 2: [2, 3]}
         df = pd.DataFrame(data)