Skip to content

Commit 32b6701

Browse files
authored
Fix reversed color scale issues in annotated heatmap figure factory (#1251)
* Fix reversed color scale in annotated heatmap. Currently, `reversescale` does not get included in the `trace` dictionary. Therefore, the color scale doesn't change when setting it to `True` (although the color of annotations gets inverted). * Small refactor to pull out black/white colors as variables and add should_use_black_text function to hold color contrast equation * Handle reversescale when passing in a custom colorscale
1 parent c935193 commit 32b6701

File tree

2 files changed

+114
-18
lines changed

2 files changed

+114
-18
lines changed

Diff for: plotly/figure_factory/_annotated_heatmap.py

+33-18
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def create_annotated_heatmap(z, x=None, y=None, annotation_text=None,
6464
defined, the colors are defined logically as black or white
6565
depending on the heatmap's colorscale.
6666
:param (bool) showscale: Display colorscale. Default = False
67+
:param (bool) reversescale: Reverse colorscale. Default = False
6768
:param kwargs: kwargs passed through plotly.graph_objs.Heatmap.
6869
These kwargs describe other attributes about the annotated Heatmap
6970
trace such as the colorscale. For more information on valid kwargs
@@ -98,14 +99,14 @@ def create_annotated_heatmap(z, x=None, y=None, annotation_text=None,
9899

99100
if x or y:
100101
trace = dict(type='heatmap', z=z, x=x, y=y, colorscale=colorscale,
101-
showscale=showscale, **kwargs)
102+
showscale=showscale, reversescale=reversescale, **kwargs)
102103
layout = dict(annotations=annotations,
103104
xaxis=dict(ticks='', dtick=1, side='top',
104105
gridcolor='rgb(0, 0, 0)'),
105106
yaxis=dict(ticks='', dtick=1, ticksuffix=' '))
106107
else:
107108
trace = dict(type='heatmap', z=z, colorscale=colorscale,
108-
showscale=showscale, **kwargs)
109+
showscale=showscale, reversescale=reversescale, **kwargs)
109110
layout = dict(annotations=annotations,
110111
xaxis=dict(ticks='', side='top',
111112
gridcolor='rgb(0, 0, 0)',
@@ -127,6 +128,12 @@ def to_rgb_color_list(color_str, default):
127128
return default
128129

129130

131+
def should_use_black_text(background_color):
132+
return (background_color[0] * 0.299 +
133+
background_color[1] * 0.587 +
134+
background_color[2] * 0.114) > 186
135+
136+
130137
class _AnnotatedHeatmap(object):
131138
"""
132139
Refer to TraceFactory.create_annotated_heatmap() for docstring
@@ -173,39 +180,47 @@ def get_text_color(self):
173180
'Earth', 'Electric', 'Viridis', 'Cividis']
174181
# Plotly colorscales ranging from a darker shade to a lighter shade
175182
colorscales_reverse = ['Reds']
183+
184+
white = '#FFFFFF'
185+
black = '#000000'
176186
if self.font_colors:
177187
min_text_color = self.font_colors[0]
178188
max_text_color = self.font_colors[-1]
179189
elif self.colorscale in colorscales and self.reversescale:
180-
min_text_color = '#000000'
181-
max_text_color = '#FFFFFF'
190+
min_text_color = black
191+
max_text_color = white
182192
elif self.colorscale in colorscales:
183-
min_text_color = '#FFFFFF'
184-
max_text_color = '#000000'
193+
min_text_color = white
194+
max_text_color = black
185195
elif self.colorscale in colorscales_reverse and self.reversescale:
186-
min_text_color = '#FFFFFF'
187-
max_text_color = '#000000'
196+
min_text_color = white
197+
max_text_color = black
188198
elif self.colorscale in colorscales_reverse:
189-
min_text_color = '#000000'
190-
max_text_color = '#FFFFFF'
199+
min_text_color = black
200+
max_text_color = white
191201
elif isinstance(self.colorscale, list):
192202

193203
min_col = to_rgb_color_list(self.colorscale[0][1],
194204
[255, 255, 255])
195205
max_col = to_rgb_color_list(self.colorscale[-1][1],
196206
[255, 255, 255])
197207

198-
if (min_col[0]*0.299 + min_col[1]*0.587 + min_col[2]*0.114) > 186:
199-
min_text_color = '#000000'
208+
# swap min/max colors if reverse scale
209+
if self.reversescale:
210+
min_col, max_col = max_col, min_col
211+
212+
if should_use_black_text(min_col):
213+
min_text_color = black
200214
else:
201-
min_text_color = '#FFFFFF'
202-
if (max_col[0]*0.299 + max_col[1]*0.587 + max_col[2]*0.114) > 186:
203-
max_text_color = '#000000'
215+
min_text_color = white
216+
217+
if should_use_black_text(max_col):
218+
max_text_color = black
204219
else:
205-
max_text_color = '#FFFFFF'
220+
max_text_color = white
206221
else:
207-
min_text_color = '#000000'
208-
max_text_color = '#000000'
222+
min_text_color = black
223+
max_text_color = black
209224
return min_text_color, max_text_color
210225

211226
def get_z_mid(self):

Diff for: plotly/tests/test_optional/test_tools/test_figure_factory.py

+81
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,7 @@ def test_simple_annotated_heatmap(self):
756756
expected_a_heat = {
757757
'data': [{'colorscale': 'RdBu',
758758
'showscale': False,
759+
'reversescale': False,
759760
'type': 'heatmap',
760761
'z': [[1, 0, 0.5], [0.25, 0.75, 0.45]]}],
761762
'layout': {'annotations': [{'font': {'color': '#000000'},
@@ -831,6 +832,7 @@ def test_annotated_heatmap_kwargs(self):
831832
expected_a = {'data': [{'colorscale':
832833
[[0, 'rgb(255,255,255)'], [1, '#e6005a']],
833834
'showscale': False,
835+
'reversescale': False,
834836
'type': 'heatmap',
835837
'x': ['A', 'B'],
836838
'y': ['One', 'Two', 'Three'],
@@ -891,6 +893,85 @@ def test_annotated_heatmap_kwargs(self):
891893
self.assert_fig_equal(a['layout'],
892894
expected_a['layout'])
893895

896+
def test_annotated_heatmap_reversescale(self):
897+
898+
# we should be able to create an annotated heatmap with x and y axes
899+
# lables, a defined colorscale, and supplied text.
900+
901+
z = [[1, 0], [.25, .75], [.45, .5]]
902+
text = [['first', 'second'], ['third', 'fourth'], ['fifth', 'sixth']]
903+
a = ff.create_annotated_heatmap(z,
904+
x=['A', 'B'],
905+
y=['One', 'Two', 'Three'],
906+
annotation_text=text,
907+
reversescale=True,
908+
colorscale=[[0, 'rgb(255,255,255)'],
909+
[1, '#e6005a']])
910+
expected_a = {'data': [{'colorscale':
911+
[[0, 'rgb(255,255,255)'], [1, '#e6005a']],
912+
'showscale': False,
913+
'reversescale': True,
914+
'type': 'heatmap',
915+
'x': ['A', 'B'],
916+
'y': ['One', 'Two', 'Three'],
917+
'z': [[1, 0], [0.25, 0.75], [0.45, 0.5]]}],
918+
'layout': {'annotations': [
919+
{'font': {'color': '#000000'},
920+
'showarrow': False,
921+
'text': 'first',
922+
'x': 'A',
923+
'xref': 'x',
924+
'y': 'One',
925+
'yref': 'y'},
926+
{'font': {'color': '#FFFFFF'},
927+
'showarrow': False,
928+
'text': 'second',
929+
'x': 'B',
930+
'xref': 'x',
931+
'y': 'One',
932+
'yref': 'y'},
933+
{'font': {'color': '#FFFFFF'},
934+
'showarrow': False,
935+
'text': 'third',
936+
'x': 'A',
937+
'xref': 'x',
938+
'y': 'Two',
939+
'yref': 'y'},
940+
{'font': {'color': '#000000'},
941+
'showarrow': False,
942+
'text': 'fourth',
943+
'x': 'B',
944+
'xref': 'x',
945+
'y': 'Two',
946+
'yref': 'y'},
947+
{'font': {'color': '#FFFFFF'},
948+
'showarrow': False,
949+
'text': 'fifth',
950+
'x': 'A',
951+
'xref': 'x',
952+
'y': 'Three',
953+
'yref': 'y'},
954+
{'font': {'color': '#FFFFFF'},
955+
'showarrow': False,
956+
'text': 'sixth',
957+
'x': 'B',
958+
'xref': 'x',
959+
'y': 'Three',
960+
'yref': 'y'}],
961+
'xaxis': {'dtick': 1,
962+
'gridcolor': 'rgb(0, 0, 0)',
963+
'side': 'top',
964+
'ticks': ''},
965+
'yaxis': {'dtick': 1, 'ticks': '',
966+
'ticksuffix': ' '}}}
967+
self.assert_fig_equal(
968+
a['data'][0],
969+
expected_a['data'][0],
970+
)
971+
972+
self.assert_fig_equal(a['layout'],
973+
expected_a['layout'])
974+
894975

895976
class TestTable(TestCase, NumpyTestUtilsMixin):
896977

0 commit comments

Comments
 (0)