From ff75dfc0d15bf35f2fc0eba1483f825956ffe1d6 Mon Sep 17 00:00:00 2001 From: Igor Zakhlebin Date: Thu, 26 Oct 2017 17:05:50 -0500 Subject: [PATCH 1/4] Fix reversed color scale in annotated heatmap Currently, `reversescale` does not get included into the `trace` dictionary. Therefore, color scale doesn't change when setting it to `True` (although the color of annotations gets inverted). --- plotly/figure_factory/_annotated_heatmap.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plotly/figure_factory/_annotated_heatmap.py b/plotly/figure_factory/_annotated_heatmap.py index ba1985fd704..5e3e36c42df 100644 --- a/plotly/figure_factory/_annotated_heatmap.py +++ b/plotly/figure_factory/_annotated_heatmap.py @@ -63,6 +63,7 @@ def create_annotated_heatmap(z, x=None, y=None, annotation_text=None, defined, the colors are defined logically as black or white depending on the heatmap's colorscale. :param (bool) showscale: Display colorscale. Default = False + :param (bool) reversescale: Reverse 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 @@ -92,14 +93,14 @@ def create_annotated_heatmap(z, x=None, y=None, annotation_text=None, if x or y: trace = dict(type='heatmap', z=z, x=x, y=y, colorscale=colorscale, - showscale=showscale, **kwargs) + showscale=showscale, reversescale=reversescale, **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) + showscale=showscale, reversescale=reversescale, **kwargs) layout = dict(annotations=annotations, xaxis=dict(ticks='', side='top', gridcolor='rgb(0, 0, 0)', From 31efc820ab11bb759fe0d38cef09bee154097602 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Oct 2018 19:40:19 -0400 Subject: [PATCH 2/4] Fix tests --- plotly/tests/test_optional/test_tools/test_figure_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plotly/tests/test_optional/test_tools/test_figure_factory.py b/plotly/tests/test_optional/test_tools/test_figure_factory.py index e9a11ff0ae6..9f723d56b3c 100644 --- a/plotly/tests/test_optional/test_tools/test_figure_factory.py +++ b/plotly/tests/test_optional/test_tools/test_figure_factory.py @@ -756,6 +756,7 @@ def test_simple_annotated_heatmap(self): expected_a_heat = { 'data': [{'colorscale': 'RdBu', 'showscale': False, + 'reversescale': False, 'type': 'heatmap', 'z': [[1, 0, 0.5], [0.25, 0.75, 0.45]]}], 'layout': {'annotations': [{'font': {'color': '#000000'}, @@ -831,6 +832,7 @@ def test_annotated_heatmap_kwargs(self): expected_a = {'data': [{'colorscale': [[0, 'rgb(255,255,255)'], [1, '#e6005a']], 'showscale': False, + 'reversescale': False, 'type': 'heatmap', 'x': ['A', 'B'], 'y': ['One', 'Two', 'Three'], From 5fbb12193f141f848d5b0a4844a00404a8584caf Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Oct 2018 19:42:21 -0400 Subject: [PATCH 3/4] Small refactor - pull out black/white colors as variables - Add should_use_black_text function to hold color contrast equation --- plotly/figure_factory/_annotated_heatmap.py | 42 +++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/plotly/figure_factory/_annotated_heatmap.py b/plotly/figure_factory/_annotated_heatmap.py index 639bc50cea9..684c77d9a94 100644 --- a/plotly/figure_factory/_annotated_heatmap.py +++ b/plotly/figure_factory/_annotated_heatmap.py @@ -128,6 +128,12 @@ def to_rgb_color_list(color_str, default): return default +def should_use_black_text(background_color): + return (background_color[0] * 0.299 + + background_color[1] * 0.587 + + background_color[2] * 0.114) > 186 + + class _AnnotatedHeatmap(object): """ Refer to TraceFactory.create_annotated_heatmap() for docstring @@ -174,21 +180,24 @@ def get_text_color(self): 'Earth', 'Electric', 'Viridis', 'Cividis'] # Plotly colorscales ranging from a darker shade to a lighter shade colorscales_reverse = ['Reds'] + + white = '#FFFFFF' + black = '#000000' 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' + min_text_color = black + max_text_color = white elif self.colorscale in colorscales: - min_text_color = '#FFFFFF' - max_text_color = '#000000' + min_text_color = white + max_text_color = black elif self.colorscale in colorscales_reverse and self.reversescale: - min_text_color = '#FFFFFF' - max_text_color = '#000000' + min_text_color = white + max_text_color = black elif self.colorscale in colorscales_reverse: - min_text_color = '#000000' - max_text_color = '#FFFFFF' + min_text_color = black + max_text_color = white elif isinstance(self.colorscale, list): min_col = to_rgb_color_list(self.colorscale[0][1], @@ -196,17 +205,18 @@ def get_text_color(self): max_col = to_rgb_color_list(self.colorscale[-1][1], [255, 255, 255]) - if (min_col[0]*0.299 + min_col[1]*0.587 + min_col[2]*0.114) > 186: - min_text_color = '#000000' + if should_use_black_text(min_col): + min_text_color = black 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' + min_text_color = white + + if should_use_black_text(max_col): + max_text_color = black else: - max_text_color = '#FFFFFF' + max_text_color = white else: - min_text_color = '#000000' - max_text_color = '#000000' + min_text_color = black + max_text_color = black return min_text_color, max_text_color def get_z_mid(self): From 8fa7c6a7b787fc5d2bd943f7759d1979be0b4464 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 30 Oct 2018 19:43:01 -0400 Subject: [PATCH 4/4] Handle reversescale when passing in a custom colorscale --- plotly/figure_factory/_annotated_heatmap.py | 4 + .../test_tools/test_figure_factory.py | 79 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/plotly/figure_factory/_annotated_heatmap.py b/plotly/figure_factory/_annotated_heatmap.py index 684c77d9a94..3400aba0e48 100644 --- a/plotly/figure_factory/_annotated_heatmap.py +++ b/plotly/figure_factory/_annotated_heatmap.py @@ -205,6 +205,10 @@ def get_text_color(self): max_col = to_rgb_color_list(self.colorscale[-1][1], [255, 255, 255]) + # swap min/max colors if reverse scale + if self.reversescale: + min_col, max_col = max_col, min_col + if should_use_black_text(min_col): min_text_color = black else: diff --git a/plotly/tests/test_optional/test_tools/test_figure_factory.py b/plotly/tests/test_optional/test_tools/test_figure_factory.py index 9f723d56b3c..36ae36eaf9e 100644 --- a/plotly/tests/test_optional/test_tools/test_figure_factory.py +++ b/plotly/tests/test_optional/test_tools/test_figure_factory.py @@ -893,6 +893,85 @@ def test_annotated_heatmap_kwargs(self): self.assert_fig_equal(a['layout'], expected_a['layout']) + def test_annotated_heatmap_reversescale(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 = ff.create_annotated_heatmap(z, + x=['A', 'B'], + y=['One', 'Two', 'Three'], + annotation_text=text, + reversescale=True, + colorscale=[[0, 'rgb(255,255,255)'], + [1, '#e6005a']]) + expected_a = {'data': [{'colorscale': + [[0, 'rgb(255,255,255)'], [1, '#e6005a']], + 'showscale': False, + 'reversescale': True, + 'type': 'heatmap', + 'x': ['A', 'B'], + 'y': ['One', 'Two', 'Three'], + 'z': [[1, 0], [0.25, 0.75], [0.45, 0.5]]}], + 'layout': {'annotations': [ + {'font': {'color': '#000000'}, + 'showarrow': False, + 'text': 'first', + 'x': 'A', + 'xref': 'x', + 'y': 'One', + 'yref': 'y'}, + {'font': {'color': '#FFFFFF'}, + 'showarrow': False, + 'text': 'second', + 'x': 'B', + 'xref': 'x', + 'y': 'One', + 'yref': 'y'}, + {'font': {'color': '#FFFFFF'}, + 'showarrow': False, + 'text': 'third', + 'x': 'A', + 'xref': 'x', + 'y': 'Two', + 'yref': 'y'}, + {'font': {'color': '#000000'}, + 'showarrow': False, + 'text': 'fourth', + 'x': 'B', + 'xref': 'x', + 'y': 'Two', + 'yref': 'y'}, + {'font': {'color': '#FFFFFF'}, + 'showarrow': False, + 'text': 'fifth', + 'x': 'A', + 'xref': 'x', + 'y': 'Three', + 'yref': 'y'}, + {'font': {'color': '#FFFFFF'}, + 'showarrow': False, + 'text': 'sixth', + 'x': 'B', + 'xref': 'x', + 'y': 'Three', + 'yref': 'y'}], + 'xaxis': {'dtick': 1, + 'gridcolor': 'rgb(0, 0, 0)', + 'side': 'top', + 'ticks': ''}, + 'yaxis': {'dtick': 1, 'ticks': '', + 'ticksuffix': ' '}}} + self.assert_fig_equal( + a['data'][0], + expected_a['data'][0], + ) + + self.assert_fig_equal(a['layout'], + expected_a['layout']) + class TestTable(TestCase, NumpyTestUtilsMixin):