-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Color text based on background color when using _background_gradient()
#21263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
36dd331
04d30f8
6ce03f3
a878359
e7e8444
ee79b10
075cd54
5023dd6
163c364
ba6f981
a6dc14d
8993dff
6eb0930
c30255b
2c34fb9
ae2e849
7be4559
41911af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -863,7 +863,7 @@ def highlight_null(self, null_color='red'): | |
return self | ||
|
||
def background_gradient(self, cmap='PuBu', low=0, high=0, axis=0, | ||
subset=None): | ||
subset=None, text_color_threshold=0.408): | ||
""" | ||
Color the background in a gradient according to | ||
the data in each column (optionally row). | ||
|
@@ -879,26 +879,39 @@ def background_gradient(self, cmap='PuBu', low=0, high=0, axis=0, | |
1 or 'columns' for columnwise, 0 or 'index' for rowwise | ||
subset: IndexSlice | ||
a valid slice for ``data`` to limit the style application to | ||
text_color_threshold: float or int | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add And may a note as to why that's the default? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We chose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries, was just curious. |
||
luminance threshold for determining text color. Facilitates text | ||
visibility across varying background colors. From 0 to 1. | ||
0 = all text is dark colored, 1 = all text is light colored. | ||
|
||
.. versionadded:: 0.24.0 | ||
|
||
Returns | ||
------- | ||
self : Styler | ||
|
||
Notes | ||
----- | ||
Tune ``low`` and ``high`` to keep the text legible by | ||
not using the entire range of the color map. These extend | ||
the range of the data by ``low * (x.max() - x.min())`` | ||
and ``high * (x.max() - x.min())`` before normalizing. | ||
Set ``text_color_threshold`` or tune ``low`` and ``high`` to keep the | ||
text legible by not using the entire range of the color map. The range | ||
of the data is extended by ``low * (x.max() - x.min())`` and ``high * | ||
(x.max() - x.min())`` before normalizing. | ||
|
||
Raises | ||
------ | ||
ValueError | ||
If ``text_color_threshold`` is not a value from 0 to 1. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Single backtick for parameter names. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I did double tick because I saw it in other places, for example for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I noticed that after posting. Probably fine to be consistent w/ the rest of the docstring here. |
||
""" | ||
subset = _maybe_numeric_slice(self.data, subset) | ||
subset = _non_reducing_slice(subset) | ||
self.apply(self._background_gradient, cmap=cmap, subset=subset, | ||
axis=axis, low=low, high=high) | ||
axis=axis, low=low, high=high, | ||
text_color_threshold=text_color_threshold) | ||
return self | ||
|
||
@staticmethod | ||
def _background_gradient(s, cmap='PuBu', low=0, high=0): | ||
def _background_gradient(s, cmap='PuBu', low=0, high=0, | ||
text_color_threshold=0.408): | ||
"""Color background in a range according to the data.""" | ||
with _mpl(Styler.background_gradient) as (plt, colors): | ||
rng = s.max() - s.min() | ||
|
@@ -909,8 +922,39 @@ def _background_gradient(s, cmap='PuBu', low=0, high=0): | |
# https://github.com/matplotlib/matplotlib/issues/5427 | ||
normed = norm(s.values) | ||
c = [colors.rgb2hex(x) for x in plt.cm.get_cmap(cmap)(normed)] | ||
return ['background-color: {color}'.format(color=color) | ||
for color in c] | ||
if (not isinstance(text_color_threshold, (float, int)) or | ||
not 0 <= text_color_threshold <= 1): | ||
msg = "`text_color_threshold` must be a value from 0 to 1." | ||
raise ValueError(msg) | ||
|
||
def relative_luminance(color): | ||
""" | ||
Calculate relative luminance of a color. | ||
|
||
The calculation adheres to the W3C standards | ||
(https://www.w3.org/WAI/GL/wiki/Relative_luminance) | ||
|
||
Parameters | ||
---------- | ||
color : matplotlib color | ||
Hex code, rgb-tuple, or HTML color name. | ||
|
||
Returns | ||
------- | ||
float | ||
The relative luminance as a value from 0 to 1 | ||
""" | ||
rgb = colors.colorConverter.to_rgba_array(color)[:, :3] | ||
rgb = np.where(rgb <= .03928, rgb / 12.92, | ||
((rgb + .055) / 1.055) ** 2.4) | ||
lum = rgb.dot([.2126, .7152, .0722]) | ||
return lum.item() | ||
|
||
text_colors = ['#f1f1f1' if relative_luminance(x) < | ||
text_color_threshold else '#000000' for x in c] | ||
|
||
return ['background-color: {color};color: {tc}'.format( | ||
color=color, tc=tc) for color, tc in zip(c, text_colors)] | ||
|
||
def set_properties(self, subset=None, **kwargs): | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1017,9 +1017,9 @@ def test_hide_columns_mult_levels(self): | |
assert ctx['body'][1][2]['display_value'] == 3 | ||
|
||
|
||
@td.skip_if_no_mpl | ||
class TestStylerMatplotlibDep(object): | ||
|
||
@td.skip_if_no_mpl | ||
def test_background_gradient(self): | ||
df = pd.DataFrame([[1, 2], [2, 4]], columns=['A', 'B']) | ||
|
||
|
@@ -1031,7 +1031,30 @@ def test_background_gradient(self): | |
|
||
result = df.style.background_gradient( | ||
subset=pd.IndexSlice[1, 'A'])._compute().ctx | ||
assert result[(1, 0)] == ['background-color: #fff7fb'] | ||
|
||
assert result[(1, 0)] == ['background-color: #fff7fb', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a more comprehensive / dedicated test for this? Something that encompasses the full range of expected values There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a suggestion |
||
'color: #000000'] | ||
|
||
@pytest.mark.parametrize( | ||
'c_map,expected', [ | ||
(None, { | ||
(0, 0): ['background-color: #440154', 'color: #f1f1f1'], | ||
(1, 0): ['background-color: #fde725', 'color: #000000']}), | ||
('YlOrRd', { | ||
(0, 0): ['background-color: #ffffcc', 'color: #000000'], | ||
(1, 0): ['background-color: #800026', 'color: #f1f1f1']})]) | ||
def test_text_color_threshold(self, c_map, expected): | ||
df = pd.DataFrame([1, 2], columns=['A']) | ||
result = df.style.background_gradient(cmap=c_map)._compute().ctx | ||
assert result == expected | ||
|
||
@pytest.mark.parametrize("text_color_threshold", [1.1, '1', -1, [2, 2]]) | ||
def test_text_color_threshold_raises(self, text_color_threshold): | ||
df = pd.DataFrame([[1, 2], [2, 4]], columns=['A', 'B']) | ||
msg = "`text_color_threshold` must be a value from 0 to 1." | ||
with tm.assert_raises_regex(ValueError, msg): | ||
df.style.background_gradient( | ||
text_color_threshold=text_color_threshold)._compute() | ||
|
||
|
||
def test_block_names(): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add a Raises section for the docstring?