Repository: pandas-dev/pandas

Issue:
Automatically adjust HTML table text color when using `style.background_gradient()`
#### Code Sample

```python
import pandas as pd
import numpy as np

np.random.seed(24)
df = pd.DataFrame(np.random.randn(8, 4), columns=list('ABCD'))
df.style.background_gradient()
```
![image](https://user-images.githubusercontent.com/4560057/40730971-0328c8d8-63fe-11e8-9966-3e7e06d58eb6.png)

#### Problem description

Since all text in the displayed HTML table is black, it is difficult to see values when the background is dark. This is currently possible to adjust manually with the `high` and `low` parameters, but this limits the range of the colormap.

A possible improvement would be to color text white automatically if the background color is too dark (low luminance). This approach is currently [implemented for annotated heatmaps](https://github.com/mwaskom/seaborn/blob/scatterplot/seaborn/matrix.py#L241-L242) in the visualization library `seaborn`, and similar criteria could be used with `pandas` styles ([luminance implementation](https://github.com/mwaskom/seaborn/blob/scatterplot/seaborn/utils.py#L547-L566) and [w3c's official definition](https://www.w3.org/WAI/GL/wiki/Relative_luminance)).

Automatic text coloring could be exposed through a `text_color` parameter to `style.background_gradient()`, which could be either `auto`, or a color value. Another alternative is to let the user adjust the luminance threshold directly, with a sound default value. A third approach would be to create a new method for controlling the text color in general, and call it with specific arguments inside `style.background_gradient()`.

If you think adding this functionality would be useful, I am happy to attempt a PR.

#### Expected Output

luminance threshold 0.408 (`seaborn` default)
![screenshot-20180530-112153](https://user-images.githubusercontent.com/4560057/40730034-bc29faf8-63fb-11e8-90f5-2cd3df94b638.png)

luminance threshold 0.2
![screenshot-20180530-112028](https://user-images.githubusercontent.com/4560057/40730036-bc487334-63fb-11e8-87be-82e9e5ab7e8a.png)


#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: c85ab083919b59ce84c220d5baf7d34ff4a0bcf2
python: 3.6.5.final.0
python-bits: 64
OS: Linux
OS-release: 4.16.8-1-ARCH
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.21.0.dev+2049.gc85ab0839.dirty
pytest: 3.5.1
pip: 9.0.3
setuptools: 39.2.0
Cython: 0.28.2
numpy: 1.14.3
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.4.0
sphinx: 1.7.4
patsy: 0.5.0
dateutil: 2.7.3
pytz: 2018.4
blosc: None
bottleneck: 1.2.1
tables: 3.4.3
numexpr: 2.6.5
feather: None
matplotlib: 2.2.2
openpyxl: 2.5.3
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.4
lxml: 4.2.1
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.7
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>

Automatically adjust HTML table text color when using `style.background_gradient()`
#### Code Sample

```python
import pandas as pd
import numpy as np

np.random.seed(24)
df = pd.DataFrame(np.random.randn(8, 4), columns=list('ABCD'))
df.style.background_gradient()
```
![image](https://user-images.githubusercontent.com/4560057/40730971-0328c8d8-63fe-11e8-9966-3e7e06d58eb6.png)

#### Problem description

Since all text in the displayed HTML table is black, it is difficult to see values when the background is dark. This is currently possible to adjust manually with the `high` and `low` parameters, but this limits the range of the colormap.

A possible improvement would be to color text white automatically if the background color is too dark (low luminance). This approach is currently [implemented for annotated heatmaps](https://github.com/mwaskom/seaborn/blob/scatterplot/seaborn/matrix.py#L241-L242) in the visualization library `seaborn`, and similar criteria could be used with `pandas` styles ([luminance implementation](https://github.com/mwaskom/seaborn/blob/scatterplot/seaborn/utils.py#L547-L566) and [w3c's official definition](https://www.w3.org/WAI/GL/wiki/Relative_luminance)).

Automatic text coloring could be exposed through a `text_color` parameter to `style.background_gradient()`, which could be either `auto`, or a color value. Another alternative is to let the user adjust the luminance threshold directly, with a sound default value. A third approach would be to create a new method for controlling the text color in general, and call it with specific arguments inside `style.background_gradient()`.

If you think adding this functionality would be useful, I am happy to attempt a PR.

#### Expected Output

luminance threshold 0.408 (`seaborn` default)
![screenshot-20180530-112153](https://user-images.githubusercontent.com/4560057/40730034-bc29faf8-63fb-11e8-90f5-2cd3df94b638.png)

luminance threshold 0.2
![screenshot-20180530-112028](https://user-images.githubusercontent.com/4560057/40730036-bc487334-63fb-11e8-87be-82e9e5ab7e8a.png)


#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: c85ab083919b59ce84c220d5baf7d34ff4a0bcf2
python: 3.6.5.final.0
python-bits: 64
OS: Linux
OS-release: 4.16.8-1-ARCH
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.21.0.dev+2049.gc85ab0839.dirty
pytest: 3.5.1
pip: 9.0.3
setuptools: 39.2.0
Cython: 0.28.2
numpy: 1.14.3
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.4.0
sphinx: 1.7.4
patsy: 0.5.0
dateutil: 2.7.3
pytz: 2018.4
blosc: None
bottleneck: 1.2.1
tables: 3.4.3
numexpr: 2.6.5
feather: None
matplotlib: 2.2.2
openpyxl: 2.5.3
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.4
lxml: 4.2.1
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.7
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
I think it's reasonable - if you want to try your hand at a PR then please do!
I think it's reasonable - if you want to try your hand at a PR then please do!

Resolving patch:
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -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,6 +879,12 @@ 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
+            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
         -------
@@ -886,19 +892,26 @@ def background_gradient(self, cmap='PuBu', low=0, high=0, axis=0,
 
         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.
         """
         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):
         """


Test patch:
diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py
--- a/pandas/tests/io/formats/test_style.py
+++ b/pandas/tests/io/formats/test_style.py
@@ -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',
+                                  '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():