diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cdf5ed2b46..0f97382ec45 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/tests/test_core/test_tools/test_figure_factory.py b/plotly/tests/test_core/test_tools/test_figure_factory.py index 3616eb1e9cf..32744650bcb 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 @@ def test_datetime_candlestick(self): 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': 'Country', + 'x': -0.45, + 'xanchor': 'left', + 'xref': 'x1', + 'y': 0, + 'yref': 'y1'}, + {'align': 'left', + 'font': {'color': '#ffffff'}, + 'showarrow': False, + 'text': 'Year', + 'x': 0.55, + 'xanchor': 'left', + 'xref': 'x1', + 'y': 0, + 'yref': 'y1'}, + {'align': 'left', + 'font': {'color': '#ffffff'}, + 'showarrow': False, + 'text': 'Population', + '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': 'Country', + 'x': -0.45, + 'xanchor': 'left', + 'xref': 'x1', + 'y': 0, + 'yref': 'y1'}, + {'align': 'left', + 'font': {'color': '#ffffff'}, + 'showarrow': False, + 'text': 'Year', + 'x': 0.55, + 'xanchor': 'left', + 'xref': 'x1', + 'y': 0, + 'yref': 'y1'}, + {'align': 'left', + 'font': {'color': '#ffffff'}, + 'showarrow': False, + 'text': 'Population', + 'x': 1.55, + 'xanchor': 'left', + 'xref': 'x1', + 'y': 0, + 'yref': 'y1'}, + {'align': 'left', + 'font': {'color': '#ffffff'}, + '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': '#ffffff'}, + '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'}], + '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): diff --git a/plotly/tools.py b/plotly/tools.py index f26dc7d3e43..71835639ec6 100644 --- a/plotly/tools.py +++ b/plotly/tools.py @@ -49,6 +49,12 @@ def warning_on_one_line(message, category, filename, lineno, 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 @@ def _validate_ohlc(open, high, low, close, direction, **kwargs): @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 @@ def _validate_positive_scalars(**kwargs): @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 @@ def _validate_streamline(x, y): 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 @@ def _flatten(array): "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 @@ def create_dendrogram(X, orientation="bottom", labels=None, 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 @@ def sum_streamlines(self): 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 @@ def get_dendrogram_traces(self, X, colorscale): 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 = ('' + str(val) + '' 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 e5102d30158..35424e875ea 100644 --- a/plotly/version.py +++ b/plotly/version.py @@ -1 +1 @@ -__version__ = '1.9.0' +__version__ = '1.9.1'