Repository: plotly/plotly.py
Pull request #359 (2015-11-25 01:04:20)

Annotated heatmap
- add annotated heatmaps and tables to FigureFactory


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8cdf5ed2b..0f97382ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [Unreleased]
 
+## [1.9.1] - 2015-11-26
+### Added
+- The FigureFactory can now create annotated heatmaps with `.create_annotated_heatmap`.
+- The FigureFactory can now create tables with `.create_table`.
+
 ## [1.9.0] - 2015-11-15
 - Previously, using plotly offline required a paid license.
 No more: `plotly.js` is now shipped inside this package to allow
diff --git a/plotly/tools.py b/plotly/tools.py
index f26dc7d3e..71835639e 100644
--- a/plotly/tools.py
+++ b/plotly/tools.py
@@ -49,6 +49,12 @@ try:
 except ImportError:
     _numpy_imported = False
 
+try:
+    import pandas as pd
+    _pandas_imported = True
+except ImportError:
+    _pandas_imported = False
+
 try:
     import scipy as scp
     _scipy_imported = True
@@ -1428,10 +1434,12 @@ class FigureFactory(object):
     without notice.
 
     Supported chart types include candlestick, open high low close, quiver,
-    and streamline. See FigureFactory.create_candlestick,
-    FigureFactory.create_ohlc, FigureFactory.create_quiver, or
-    FigureFactory.create_streamline for for more infomation and examples of a
-    specific chart type.
+    streamline, distplot, dendrogram, annotated heatmap, and tables. See
+    FigureFactory.create_candlestick, FigureFactory.create_ohlc,
+    FigureFactory.create_quiver, FigureFactory.create_streamline,
+    FigureFactory.create_distplot, FigureFactory.create_dendrogram,
+    FigureFactory.create_annotated_heatmap, or FigureFactory.create_table for
+    more information and examples of a specific chart type.
     """
 
     @staticmethod
@@ -1492,7 +1500,7 @@ class FigureFactory(object):
     @staticmethod
     def _validate_distplot(hist_data, curve_type):
         """
-        distplot specific validations
+        Distplot-specific validations
 
         :raises: (PlotlyError) If hist_data is not a list of lists
         :raises: (PlotlyError) If curve_type is not valid (i.e. not 'kde' or
@@ -1546,7 +1554,7 @@ class FigureFactory(object):
     @staticmethod
     def _validate_streamline(x, y):
         """
-        streamline specific validations
+        Streamline-specific validations
 
         Specifically, this checks that x and y are both evenly spaced,
         and that the package numpy is available.
@@ -1569,6 +1577,55 @@ class FigureFactory(object):
                 raise exceptions.PlotlyError("y must be a 1 dimensional, "
                                              "evenly spaced array")
 
+    @staticmethod
+    def _validate_annotated_heatmap(z, x, y, annotation_text):
+        """
+        Annotated-heatmap-specific validations
+
+        Check that if a text matrix is supplied, it has the same
+        dimensions as the z matrix.
+
+        See FigureFactory.create_annotated_heatmap() for params
+
+        :raises: (PlotlyError) If z and text matrices do not  have the same
+            dimensions.
+        """
+        if annotation_text is not None and isinstance(annotation_text, list):
+            FigureFactory._validate_equal_length(z, annotation_text)
+            for lst in range(len(z)):
+                if len(z[lst]) != len(annotation_text[lst]):
+                    raise exceptions.PlotlyError("z and text should have the "
+                                                 "same dimensions")
+
+        if x:
+            if len(x) != len(z[0]):
+                raise exceptions.PlotlyError("oops, the x list that you "
+                                             "provided does not match the "
+                                             "width of your z matrix ")
+
+        if y:
+            if len(y) != len(z):
+                raise exceptions.PlotlyError("oops, the y list that you "
+                                             "provided does not match the "
+                                             "length of your z matrix ")
+
+    @staticmethod
+    def _validate_table(table_text, font_colors):
+        """
+        Table-specific validations
+
+        Check that font_colors is supplied correctly (1, 3, or len(text)
+            colors).
+
+        :raises: (PlotlyError) If font_colors is supplied incorretly.
+
+        See FigureFactory.create_table() for params
+        """
+        font_colors_len_options = [1, 3, len(table_text)]
+        if len(font_colors) not in font_colors_len_options:
+            raise exceptions.PlotlyError("Oops, font_colors should be a list "
+                                         "of length 1, 3 or len(text)")
+
     @staticmethod
     def _flatten(array):
         """
@@ -1585,6 +1642,21 @@ class FigureFactory(object):
                                          "flattened! Make sure your data is "
                                          "entered as lists or ndarrays!")
 
+    @staticmethod
+    def _hex_to_rgb(value):
+        """
+        Calculates rgb values from a hex color code.
+
+        :param (string) value: Hex color string
+
+        :rtype (tuple) (r_value, g_value, b_value): tuple of rgb values
+        """
+        value = value.lstrip('#')
+        hex_total_length = len(value)
+        rgb_section_length = hex_total_length // 3
+        return tuple(int(value[i:i + rgb_section_length], 16)
+                     for i in range(0, hex_total_length, rgb_section_length))
+
     @staticmethod
     def create_quiver(x, y, u, v, scale=.1, arrow_scale=.3,
                       angle=math.pi / 9, **kwargs):
@@ -2509,6 +2581,189 @@ class FigureFactory(object):
         return {'layout': dendrogram.layout,
                 'data': dendrogram.data}
 
+    @staticmethod
+    def create_annotated_heatmap(z, x=None, y=None, annotation_text=None,
+                                 colorscale='RdBu', font_colors=None,
+                                 showscale=False, reversescale=False,
+                                 **kwargs):
+        """
+        BETA function that creates annotated heatmaps
+
+        This function adds annotations to each cell of the heatmap.
+
+        :param (list[list]|ndarray) z: z matrix to create heatmap.
+        :param (list) x: x axis labels.
+        :param (list) y: y axis labels.
+        :param (list[list]|ndarray) annotation_text: Text strings for
+            annotations. Should have the same dimensions as the z matrix. If no
+            text is added, the values of the z matrix are annotated. Default =
+            z matrix values.
+        :param (list|str) colorscale: heatmap colorscale.
+        :param (list) font_colors: List of two color strings: [min_text_color,
+            max_text_color] where min_text_color is applied to annotations for
+            heatmap values < (max_value - min_value)/2. If font_colors is not
+            defined, the colors are defined logically as black or white
+            depending on the heatmap's colorscale.
+        :param (bool) showscale: Display colorscale. Default = False
+        :param kwargs: kwargs passed through plotly.graph_objs.Heatmap.
+            These kwargs describe other attributes about the annotated Heatmap
+            trace such as the colorscale. For more information on valid kwargs
+            call help(plotly.graph_objs.Heatmap)
+
+        Example 1: Simple annotated heatmap with default configuration
+        ```
+        import plotly.plotly as py
+        from plotly.tools import FigureFactory as FF
+
+        z = [[0.300000, 0.00000, 0.65, 0.300000],
+             [1, 0.100005, 0.45, 0.4300],
+             [0.300000, 0.00000, 0.65, 0.300000],
+             [1, 0.100005, 0.45, 0.00000]]
+
+        figure = FF.create_annotated_heatmap(z)
+        py.iplot(figure)
+        ```
+        """
+        # TODO: protected until #282
+        from plotly.graph_objs import graph_objs
+
+        # Avoiding mutables in the call signature
+        font_colors = font_colors if font_colors is not None else []
+        FigureFactory._validate_annotated_heatmap(z, x, y, annotation_text)
+        annotations = _AnnotatedHeatmap(z, x, y, annotation_text,
+                                        colorscale, font_colors, reversescale,
+                                        **kwargs).make_annotations()
+
+        if x or y:
+            trace = dict(type='heatmap', z=z, x=x, y=y, colorscale=colorscale,
+                         showscale=showscale, **kwargs)
+            layout = dict(annotations=annotations,
+                          xaxis=dict(ticks='', dtick=1, side='top',
+                                     gridcolor='rgb(0, 0, 0)'),
+                          yaxis=dict(ticks='', dtick=1, ticksuffix='  '))
+        else:
+            trace = dict(type='heatmap', z=z, colorscale=colorscale,
+                         showscale=showscale, **kwargs)
+            layout = dict(annotations=annotations,
+                          xaxis=dict(ticks='', side='top',
+                                     gridcolor='rgb(0, 0, 0)',
+                                     showticklabels=False),
+                          yaxis=dict(ticks='', ticksuffix='  ',
+                                     showticklabels=False))
+
+        data = [trace]
+
+        return graph_objs.Figure(data=data, layout=layout)
+
+    @staticmethod
+    def create_table(table_text, colorscale=None, font_colors=None,
+                     index=False, index_title='', annotation_offset=.45,
+                     height_constant=30, hoverinfo='none', **kwargs):
+        """
+        BETA function that creates data tables
+
+        :param (pandas.Dataframe | list[list]) text: data for table.
+        :param (str|list[list]) colorscale: Colorscale for table where the
+            color at value 0 is the header color, .5 is the first table color
+            and 1 is the second table color. (Set .5 and 1 to avoid the striped
+            table effect). Default=[[0, '#66b2ff'], [.5, '#d9d9d9'],
+            [1, '#ffffff']]
+        :param (list) font_colors: Color for fonts in table. Can be a single
+            color, three colors, or a color for each row in the table.
+            Default=['#000000'] (black text for the entire table)
+        :param (int) height_constant: Constant multiplied by # of rows to
+            create table height. Default=30.
+        :param (bool) index: Create (header-colored) index column index from
+            Pandas dataframe or list[0] for each list in text. Default=False.
+        :param (string) index_title: Title for index column. Default=''.
+        :param kwargs: kwargs passed through plotly.graph_objs.Heatmap.
+            These kwargs describe other attributes about the annotated Heatmap
+            trace such as the colorscale. For more information on valid kwargs
+            call help(plotly.graph_objs.Heatmap)
+
+        Example 1: Simple Plotly Table
+        ```
+        import plotly.plotly as py
+        from plotly.tools import FigureFactory as FF
+
+        text = [['Country', 'Year', 'Population'],
+                ['US', 2000, 282200000],
+                ['Canada', 2000, 27790000],
+                ['US', 2010, 309000000],
+                ['Canada', 2010, 34000000]]
+
+        table = FF.create_table(text)
+        py.iplot(table)
+        ```
+
+        Example 2: Table with Custom Coloring
+        ```
+        import plotly.plotly as py
+        from plotly.tools import FigureFactory as FF
+
+        text = [['Country', 'Year', 'Population'],
+                ['US', 2000, 282200000],
+                ['Canada', 2000, 27790000],
+                ['US', 2010, 309000000],
+                ['Canada', 2010, 34000000]]
+
+        table = FF.create_table(text,
+                              colorscale=[[0, '#000000'],
+                                          [.5, '#80beff'],
+                                          [1, '#cce5ff']],
+                              font_colors=['#ffffff', '#000000',
+                                         '#000000'])
+        py.iplot(table)
+        ```
+        Example 3: Simple Plotly Table with Pandas
+        ```
+        import plotly.plotly as py
+        from plotly.tools import FigureFactory as FF
+
+        import pandas as pd
+
+        df = pd.read_csv('http://www.stat.ubc.ca/~jenny/notOcto/STAT545A/examples/gapminder/data/gapminderDataFiveYear.txt', sep='\t')
+        df_p = df[0:25]
+
+        table_simple = FF.create_table(df_p)
+        py.iplot(table_simple)
+        ```
+        """
+        # TODO: protected until #282
+        from plotly.graph_objs import graph_objs
+
+        # Avoiding mutables in the call signature
+        colorscale = \
+            colorscale if colorscale is not None else [[0, '#00083e'],
+                                                       [.5, '#ededee'],
+                                                       [1, '#ffffff']]
+        font_colors = font_colors if font_colors is not None else ['#ffffff',
+                                                                   '#000000',
+                                                                   '#000000']
+
+        FigureFactory._validate_table(table_text, font_colors)
+        table_matrix = _Table(table_text, colorscale, font_colors, index,
+                              index_title, annotation_offset,
+                              **kwargs).get_table_matrix()
+        annotations = _Table(table_text, colorscale, font_colors, index,
+                             index_title, annotation_offset,
+                             **kwargs).make_table_annotations()
+
+        trace = dict(type='heatmap', z=table_matrix, opacity=.75,
+                     colorscale=colorscale, showscale=False,
+                     hoverinfo=hoverinfo, **kwargs)
+
+        data = [trace]
+        layout = dict(annotations=annotations,
+                      height=len(table_matrix)*height_constant + 50,
+                      margin=dict(t=0, b=0, r=0, l=0),
+                      yaxis=dict(autorange='reversed', zeroline=False,
+                                 gridwidth=2, ticks='', dtick=1, tick0=.5,
+                                 showticklabels=False),
+                      xaxis=dict(zeroline=False, gridwidth=2, ticks='',
+                                 dtick=1, tick0=-0.5, showticklabels=False))
+        return graph_objs.Figure(data=data, layout=layout)
+
 
 class _Quiver(FigureFactory):
     """
@@ -2927,6 +3182,7 @@ class _Streamline(FigureFactory):
         streamline_y = sum(self.st_y, [])
         return streamline_x, streamline_y
 
+
 class _OHLC(FigureFactory):
     """
     Refer to FigureFactory.create_ohlc_increase() for docstring.
@@ -3442,3 +3698,237 @@ class _Dendrogram(FigureFactory):
             trace_list.append(trace)
 
         return trace_list, icoord, dcoord, ordered_labels, P['leaves']
+
+
+class _AnnotatedHeatmap(FigureFactory):
+    """
+    Refer to TraceFactory.create_annotated_heatmap() for docstring
+    """
+    def __init__(self, z, x, y, annotation_text, colorscale,
+                 font_colors, reversescale, **kwargs):
+        from plotly.graph_objs import graph_objs
+
+        self.z = z
+        if x:
+            self.x = x
+        else:
+            self.x = range(len(z[0]))
+        if y:
+            self.y = y
+        else:
+            self.y = range(len(z))
+        if annotation_text is not None:
+            self.annotation_text = annotation_text
+        else:
+            self.annotation_text = self.z
+        self.colorscale = colorscale
+        self.reversescale = reversescale
+        self.font_colors = font_colors
+
+    def get_text_color(self):
+        """
+        Get font color for annotations.
+
+        The annotated heatmap can feature two text colors: min_text_color and
+        max_text_color. The min_text_color is applied to annotations for
+        heatmap values < (max_value - min_value)/2. The user can define these
+        two colors. Otherwise the colors are defined logically as black or
+        white depending on the heatmap's colorscale.
+
+        :rtype (string, string) min_text_color, max_text_color: text
+            color for annotations for heatmap values <
+            (max_value - min_value)/2 and text color for annotations for
+            heatmap values >= (max_value - min_value)/2
+        """
+        # Plotly colorscales ranging from a lighter shade to a darker shade
+        colorscales = ['Greys', 'Greens', 'Blues',
+                       'YIGnBu', 'YIOrRd', 'RdBu',
+                       'Picnic', 'Jet', 'Hot', 'Blackbody',
+                       'Earth', 'Electric', 'Viridis']
+        # Plotly colorscales ranging from a darker shade to a lighter shade
+        colorscales_reverse = ['Reds']
+        if self.font_colors:
+            min_text_color = self.font_colors[0]
+            max_text_color = self.font_colors[-1]
+        elif self.colorscale in colorscales and self.reversescale:
+            min_text_color = '#000000'
+            max_text_color = '#FFFFFF'
+        elif self.colorscale in colorscales:
+            min_text_color = '#FFFFFF'
+            max_text_color = '#000000'
+        elif self.colorscale in colorscales_reverse and self.reversescale:
+            min_text_color = '#FFFFFF'
+            max_text_color = '#000000'
+        elif self.colorscale in colorscales_reverse:
+            min_text_color = '#000000'
+            max_text_color = '#FFFFFF'
+        elif isinstance(self.colorscale, list):
+            if 'rgb' in self.colorscale[0][1]:
+                min_col = map(int,
+                              self.colorscale[0][1].strip('rgb()').split(','))
+                max_col = map(int,
+                              self.colorscale[-1][1].strip('rgb()').split(','))
+            elif '#' in self.colorscale[0][1]:
+                    min_col = FigureFactory._hex_to_rgb(self.colorscale[0][1])
+                    max_col = FigureFactory._hex_to_rgb(self.colorscale[-1][1])
+            else:
+                min_col = [255, 255, 255]
+                max_col = [255, 255, 255]
+
+            if (min_col[0]*0.299 + min_col[1]*0.587 + min_col[2]*0.114) > 186:
+                min_text_color = '#000000'
+            else:
+                min_text_color = '#FFFFFF'
+            if (max_col[0]*0.299 + max_col[1]*0.587 + max_col[2]*0.114) > 186:
+                max_text_color = '#000000'
+            else:
+                max_text_color = '#FFFFFF'
+        else:
+            min_text_color = '#000000'
+            max_text_color = '#000000'
+        return min_text_color, max_text_color
+
+    def get_z_mid(self):
+        """
+        Get the mid value of z matrix
+
+        :rtype (float) z_avg: average val from z matrix
+        """
+        if _numpy_imported and isinstance(self.z, np.ndarray):
+            z_min = np.amin(self.z)
+            z_max = np.amax(self.z)
+        else:
+            z_min = min(min(self.z))
+            z_max = max(max(self.z))
+        z_mid = (z_max+z_min) / 2
+        return z_mid
+
+    def make_annotations(self):
+        """
+        Get annotations for each cell of the heatmap with graph_objs.Annotation
+
+        :rtype (list[dict]) annotations: list of annotations for each cell of
+            the heatmap
+        """
+        from plotly.graph_objs import graph_objs
+        min_text_color, max_text_color = _AnnotatedHeatmap.get_text_color(self)
+        z_mid = _AnnotatedHeatmap.get_z_mid(self)
+        annotations = []
+        for n, row in enumerate(self.z):
+            for m, val in enumerate(row):
+                font_color = min_text_color if val < z_mid else max_text_color
+                annotations.append(
+                    graph_objs.Annotation(
+                        text=str(self.annotation_text[n][m]),
+                        x=self.x[m],
+                        y=self.y[n],
+                        xref='x1',
+                        yref='y1',
+                        font=dict(color=font_color),
+                        showarrow=False))
+        return annotations
+
+
+class _Table(FigureFactory):
+    """
+    Refer to TraceFactory.create_table() for docstring
+    """
+    def __init__(self, table_text, colorscale, font_colors, index,
+                 index_title, annotation_offset, **kwargs):
+        from plotly.graph_objs import graph_objs
+        if _pandas_imported and isinstance(table_text, pd.DataFrame):
+            headers = table_text.columns.tolist()
+            table_text_index = table_text.index.tolist()
+            table_text = table_text.values.tolist()
+            table_text.insert(0, headers)
+            if index:
+                table_text_index.insert(0, index_title)
+                for i in range(len(table_text)):
+                    table_text[i].insert(0, table_text_index[i])
+        self.table_text = table_text
+        self.colorscale = colorscale
+        self.font_colors = font_colors
+        self.index = index
+        self.annotation_offset = annotation_offset
+        self.x = range(len(table_text[0]))
+        self.y = range(len(table_text))
+
+    def get_table_matrix(self):
+        """
+        Create z matrix to make heatmap with striped table coloring
+
+        :rtype (list[list]) table_matrix: z matrix to make heatmap with striped
+            table coloring.
+        """
+        header = [0] * len(self.table_text[0])
+        odd_row = [.5] * len(self.table_text[0])
+        even_row = [1] * len(self.table_text[0])
+        table_matrix = [None] * len(self.table_text)
+        table_matrix[0] = header
+        for i in range(1, len(self.table_text), 2):
+            table_matrix[i] = odd_row
+        for i in range(2, len(self.table_text), 2):
+            table_matrix[i] = even_row
+        if self.index:
+            for array in table_matrix:
+                array[0] = 0
+        return table_matrix
+
+    def get_table_font_color(self):
+        """
+        Fill font-color array.
+
+        Table text color can vary by row so this extends a single color or
+        creates an array to set a header color and two alternating colors to
+        create the striped table pattern.
+
+        :rtype (list[list]) all_font_colors: list of font colors for each row
+            in table.
+        """
+        if len(self.font_colors) == 1:
+            all_font_colors = self.font_colors*len(self.table_text)
+        elif len(self.font_colors) == 3:
+            all_font_colors = list(range(len(self.table_text)))
+            all_font_colors[0] = self.font_colors[0]
+            for i in range(1, len(self.table_text), 2):
+                all_font_colors[i] = self.font_colors[1]
+            for i in range(2, len(self.table_text), 2):
+                all_font_colors[i] = self.font_colors[2]
+        elif len(self.font_colors) == len(self.table_text):
+            all_font_colors = self.font_colors
+        else:
+            all_font_colors = ['#000000']*len(self.table_text)
+        return all_font_colors
+
+    def make_table_annotations(self):
+        """
+        Generate annotations to fill in table text
+
+        :rtype (list) annotations: list of annotations for each cell of the
+            table.
+        """
+        from plotly.graph_objs import graph_objs
+        table_matrix = _Table.get_table_matrix(self)
+        all_font_colors = _Table.get_table_font_color(self)
+        annotations = []
+        for n, row in enumerate(self.table_text):
+            for m, val in enumerate(row):
+                # Bold text in header and index
+                format_text = ('<b>' + str(val) + '</b>' if n == 0 or
+                               self.index and m < 1 else str(val))
+                # Match font color of index to font color of header
+                font_color = (self.font_colors[0] if self.index and m == 0
+                              else all_font_colors[n])
+                annotations.append(
+                    graph_objs.Annotation(
+                        text=format_text,
+                        x=self.x[m] - self.annotation_offset,
+                        y=self.y[n],
+                        xref='x1',
+                        yref='y1',
+                        align="left",
+                        xanchor="left",
+                        font=dict(color=font_color),
+                        showarrow=False))
+        return annotations
+
diff --git a/plotly/version.py b/plotly/version.py
index e5102d301..35424e875 100644
--- a/plotly/version.py
+++ b/plotly/version.py
@@ -1,1 +1,1 @@
-__version__ = '1.9.0'
+__version__ = '1.9.1'


Test patch:
diff --git a/plotly/tests/test_core/test_tools/test_figure_factory.py b/plotly/tests/test_core/test_tools/test_figure_factory.py
index 3616eb1e9..32744650b 100644
--- a/plotly/tests/test_core/test_tools/test_figure_factory.py
+++ b/plotly/tests/test_core/test_tools/test_figure_factory.py
@@ -686,6 +686,446 @@ class TestFinanceCharts(TestCase):
         self.assertEqual(candle, exp_candle)
 
 
+class TestAnnotatedHeatmap(TestCase):
+
+    def test_unequal_z_text_size(self):
+
+        # check: PlotlyError if z and text are not the same dimensions
+
+        kwargs = {'z': [[1, 2], [1, 2]], 'annotation_text': [[1, 2, 3], [1]]}
+        self.assertRaises(PlotlyError,
+                          tls.FigureFactory.create_annotated_heatmap,
+                          **kwargs)
+
+        kwargs = {'z': [[1], [1]], 'annotation_text': [[1], [1], [1]]}
+        self.assertRaises(PlotlyError,
+                          tls.FigureFactory.create_annotated_heatmap,
+                          **kwargs)
+
+    def test_incorrect_x_size(self):
+
+        # check: PlotlyError if x is the wrong size
+
+        kwargs = {'z': [[1, 2], [1, 2]], 'x': ['A']}
+        self.assertRaises(PlotlyError,
+                          tls.FigureFactory.create_annotated_heatmap,
+                          **kwargs)
+
+    def test_incorrect_y_size(self):
+
+        # check: PlotlyError if y is the wrong size
+
+        kwargs = {'z': [[1, 2], [1, 2]], 'y': [1, 2, 3]}
+        self.assertRaises(PlotlyError,
+                          tls.FigureFactory.create_annotated_heatmap,
+                          **kwargs)
+
+    def test_simple_annotated_heatmap(self):
+
+        # we should be able to create a heatmap with annotated values with a
+        # logical text color
+
+        z = [[1, 0, .5], [.25, .75, .45]]
+        a_heat = tls.FigureFactory.create_annotated_heatmap(z)
+        expected_a_heat = {
+            'data': [{'colorscale': 'RdBu',
+                      'showscale': False,
+                      'type': 'heatmap',
+                      'z': [[1, 0, 0.5], [0.25, 0.75, 0.45]]}],
+            'layout': {'annotations': [{'font': {'color': '#000000'},
+                                        'showarrow': False,
+                                        'text': '1',
+                                        'x': 0,
+                                        'xref': 'x1',
+                                        'y': 0,
+                                        'yref': 'y1'},
+                                       {'font': {'color': '#FFFFFF'},
+                                        'showarrow': False,
+                                        'text': '0',
+                                        'x': 1,
+                                        'xref': 'x1',
+                                        'y': 0,
+                                        'yref': 'y1'},
+                                       {'font': {'color': '#FFFFFF'},
+                                        'showarrow': False,
+                                        'text': '0.5',
+                                        'x': 2,
+                                        'xref': 'x1',
+                                        'y': 0,
+                                        'yref': 'y1'},
+                                       {'font': {'color': '#FFFFFF'},
+                                        'showarrow': False,
+                                        'text': '0.25',
+                                        'x': 0,
+                                        'xref': 'x1',
+                                        'y': 1,
+                                        'yref': 'y1'},
+                                       {'font': {'color': '#000000'},
+                                        'showarrow': False,
+                                        'text': '0.75',
+                                        'x': 1,
+                                        'xref': 'x1',
+                                        'y': 1,
+                                        'yref': 'y1'},
+                                       {'font': {'color': '#FFFFFF'},
+                                        'showarrow': False,
+                                        'text': '0.45',
+                                        'x': 2,
+                                        'xref': 'x1',
+                                        'y': 1,
+                                        'yref': 'y1'}],
+                       'xaxis': {'gridcolor': 'rgb(0, 0, 0)',
+                                 'showticklabels': False,
+                                 'side': 'top',
+                                 'ticks': ''},
+                       'yaxis': {'showticklabels': False, 'ticks': '',
+                                 'ticksuffix': '  '}}}
+        self.assertEqual(a_heat, expected_a_heat)
+
+    def test_annotated_heatmap_kwargs(self):
+
+        # we should be able to create an annotated heatmap with x and y axes
+        # lables, a defined colorscale, and supplied text.
+
+        z = [[1, 0], [.25, .75], [.45, .5]]
+        text = [['first', 'second'], ['third', 'fourth'], ['fifth', 'sixth']]
+        a = tls.FigureFactory.create_annotated_heatmap(z, x=['A', 'B'],
+                                                       y=['One', 'Two',
+                                                          'Three'],
+                                                       annotation_text=text,
+                                                       colorscale=[[0,
+                                                                    '#ffffff'],
+                                                                   [1,
+                                                                    '#e6005a']]
+                                                       )
+        expected_a = {'data': [{'colorscale': [[0, '#ffffff'], [1, '#e6005a']],
+                                'showscale': False,
+                                'type': 'heatmap',
+                                'x': ['A', 'B'],
+                                'y': ['One', 'Two', 'Three'],
+                                'z': [[1, 0], [0.25, 0.75], [0.45, 0.5]]}],
+                      'layout': {'annotations': [{'font': {'color': '#FFFFFF'},
+                                                  'showarrow': False,
+                                                  'text': 'first',
+                                                  'x': 'A',
+                                                  'xref': 'x1',
+                                                  'y': 'One',
+                                                  'yref': 'y1'},
+                                 {'font': {'color': '#000000'},
+                                  'showarrow': False,
+                                  'text': 'second',
+                                  'x': 'B',
+                                  'xref': 'x1',
+                                  'y': 'One',
+                                  'yref': 'y1'},
+                                 {'font': {'color': '#000000'},
+                                  'showarrow': False,
+                                  'text': 'third',
+                                  'x': 'A',
+                                  'xref': 'x1',
+                                  'y': 'Two',
+                                  'yref': 'y1'},
+                                 {'font': {'color': '#FFFFFF'},
+                                  'showarrow': False,
+                                  'text': 'fourth',
+                                  'x': 'B',
+                                  'xref': 'x1',
+                                  'y': 'Two',
+                                  'yref': 'y1'},
+                                 {'font': {'color': '#000000'},
+                                  'showarrow': False,
+                                  'text': 'fifth',
+                                  'x': 'A',
+                                  'xref': 'x1',
+                                  'y': 'Three',
+                                  'yref': 'y1'},
+                                 {'font': {'color': '#000000'},
+                                  'showarrow': False,
+                                  'text': 'sixth',
+                                  'x': 'B',
+                                  'xref': 'x1',
+                                  'y': 'Three',
+                                  'yref': 'y1'}],
+                                 'xaxis': {'dtick': 1,
+                                           'gridcolor': 'rgb(0, 0, 0)',
+                                           'side': 'top',
+                                           'ticks': ''},
+                                 'yaxis': {'dtick': 1, 'ticks': '',
+                                           'ticksuffix': '  '}}}
+        self.assertEqual(a, expected_a)
+
+
+class TestTable(TestCase):
+
+    def test_fontcolor_input(self):
+
+        # check: PlotlyError if fontcolor input is incorrect
+
+        kwargs = {'table_text': [['one', 'two'], [1, 2], [1, 2], [1, 2]],
+                  'fontcolor': '#000000'}
+        self.assertRaises(PlotlyError,
+                          tls.FigureFactory.create_table, **kwargs)
+
+        kwargs = {'table_text': [['one', 'two'], [1, 2], [1, 2], [1, 2]],
+                  'fontcolor': ['red', 'blue']}
+        self.assertRaises(PlotlyError,
+                          tls.FigureFactory.create_table, **kwargs)
+
+    def test_simple_table(self):
+
+        # we should be able to create a striped table by suppling a text matrix
+
+        text = [['Country', 'Year', 'Population'], ['US', 2000, 282200000],
+                ['Canada', 2000, 27790000], ['US', 1980, 226500000]]
+        table = tls.FigureFactory.create_table(text)
+        expected_table = {'data': [{'colorscale': [[0, '#00083e'],
+                                                   [0.5, '#ededee'],
+                                                   [1, '#ffffff']],
+                                    'hoverinfo': 'none',
+                                    'opacity': 0.75,
+                                    'showscale': False,
+                                    'type': 'heatmap',
+                                    'z': [[0, 0, 0], [0.5, 0.5, 0.5],
+                                          [1, 1, 1], [0.5, 0.5, 0.5]]}],
+                          'layout': {'annotations': [{'align': 'left',
+                                                      'font': {'color': '#ffffff'},
+                                                      'showarrow': False,
+                                                      'text': '<b>Country</b>',
+                                                      'x': -0.45,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 0,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#ffffff'},
+                                                      'showarrow': False,
+                                                      'text': '<b>Year</b>',
+                                                      'x': 0.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 0,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#ffffff'},
+                                                      'showarrow': False,
+                                                      'text': '<b>Population</b>',
+                                                      'x': 1.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 0,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': 'US',
+                                                      'x': -0.45,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 1,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': '2000',
+                                                      'x': 0.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 1,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': '282200000',
+                                                      'x': 1.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 1,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': 'Canada',
+                                                      'x': -0.45,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 2,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': '2000',
+                                                      'x': 0.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 2,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': '27790000',
+                                                      'x': 1.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 2,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': 'US',
+                                                      'x': -0.45,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 3,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': '1980',
+                                                      'x': 0.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 3,
+                                                      'yref': 'y1'},
+                                                     {'align': 'left',
+                                                      'font': {'color': '#000000'},
+                                                      'showarrow': False,
+                                                      'text': '226500000',
+                                                      'x': 1.55,
+                                                      'xanchor': 'left',
+                                                      'xref': 'x1',
+                                                      'y': 3,
+                                                      'yref': 'y1'}],
+                                     'height': 170,
+                                     'margin': {'b': 0, 'l': 0, 'r': 0, 't': 0},
+                                     'xaxis': {'dtick': 1,
+                                               'gridwidth': 2,
+                                               'showticklabels': False,
+                                               'tick0': -0.5,
+                                               'ticks': '',
+                                               'zeroline': False},
+                                     'yaxis': {'autorange': 'reversed',
+                                               'dtick': 1,
+                                               'gridwidth': 2,
+                                               'showticklabels': False,
+                                               'tick0': 0.5,
+                                               'ticks': '',
+                                               'zeroline': False}}}
+        self.assertEqual(table, expected_table)
+
+    def test_table_with_index(self):
+
+        # we should be able to create a striped table where the first column
+        # matches the coloring of the header
+
+        text = [['Country', 'Year', 'Population'], ['US', 2000, 282200000],
+                ['Canada', 2000, 27790000]]
+        index_table = tls.FigureFactory.create_table(text, index=True,
+                                                     index_title='Title')
+        exp_index_table = {'data': [{'colorscale': [[0, '#00083e'], [0.5, '#ededee'], [1, '#ffffff']],
+                                     'hoverinfo': 'none',
+                                     'opacity': 0.75,
+                                     'showscale': False,
+                                     'type': 'heatmap',
+                                     'z': [[0, 0, 0], [0, 0.5, 0.5], [0, 1, 1]]}],
+                           'layout': {'annotations': [{'align': 'left',
+                                      'font': {'color': '#ffffff'},
+                                      'showarrow': False,
+                                      'text': '<b>Country</b>',
+                                      'x': -0.45,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 0,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#ffffff'},
+                                      'showarrow': False,
+                                      'text': '<b>Year</b>',
+                                      'x': 0.55,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 0,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#ffffff'},
+                                      'showarrow': False,
+                                      'text': '<b>Population</b>',
+                                      'x': 1.55,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 0,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#ffffff'},
+                                      'showarrow': False,
+                                      'text': '<b>US</b>',
+                                      'x': -0.45,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 1,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#000000'},
+                                      'showarrow': False,
+                                      'text': '2000',
+                                      'x': 0.55,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 1,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#000000'},
+                                      'showarrow': False,
+                                      'text': '282200000',
+                                      'x': 1.55,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 1,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#ffffff'},
+                                      'showarrow': False,
+                                      'text': '<b>Canada</b>',
+                                      'x': -0.45,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 2,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#000000'},
+                                      'showarrow': False,
+                                      'text': '2000',
+                                      'x': 0.55,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 2,
+                                      'yref': 'y1'},
+                                     {'align': 'left',
+                                      'font': {'color': '#000000'},
+                                      'showarrow': False,
+                                      'text': '27790000',
+                                      'x': 1.55,
+                                      'xanchor': 'left',
+                                      'xref': 'x1',
+                                      'y': 2,
+                                      'yref': 'y1'}],
+                                      'height': 140,
+                                      'margin': {'b': 0, 'l': 0, 'r': 0, 't': 0},
+                                      'xaxis': {'dtick': 1,
+                                                'gridwidth': 2,
+                                                'showticklabels': False,
+                                                'tick0': -0.5,
+                                                'ticks': '',
+                                                'zeroline': False},
+                                      'yaxis': {'autorange': 'reversed',
+                                                'dtick': 1,
+                                                'gridwidth': 2,
+                                                'showticklabels': False,
+                                                'tick0': 0.5,
+                                                'ticks': '',
+                                                'zeroline': False}}}
+        self.assertEqual(index_table, exp_index_table)
+
+
 # class TestDistplot(TestCase):
 
 #     def test_scipy_import_error(self):