Repository: pandas-dev/pandas

Issue:
ENH: Allow elementwise coloring in background_gradient with axis=None
At present the axis argument to `background_gradient` only accepts 0 or 1 so that the resulting "heatmap" can only be applied columnwise or rowwise rather than to the whole table.
```
Signature: df.style.background_gradient(cmap='PuBu', low=0, high=0, axis=0, subset=None)

Parameters
----------
cmap: str or colormap
    matplotlib colormap
low, high: float
    compress the range by these values.
axis: int or str
    1 or 'columns' for colunwise, 0 or 'index' for rowwise
subset: IndexSlice
    a valid slice for ``data`` to limit the style application to
```
I propose that `axis=None` be allowed to mean applying a color gradient elementwise.


Discussion:
```
df = pd.DataFrame(np.sqrt((np.mgrid[0:11, 0:11]**2).sum(axis=0)))
```
Example of `axis=0` coloring whereas I would like the color gradient to be radially increasing like the numbers
![image](https://cloud.githubusercontent.com/assets/881019/22235047/960ec6c2-e248-11e6-940f-e37ebfb9d0df.png)

Note: This would be consistent with the axis argument to `apply`:
![image](https://cloud.githubusercontent.com/assets/881019/22235094/19078e6a-e249-11e6-9d7c-e3c0f199f8ac.png)

My workaround:
```python
def background_gradient(self, cvals=None, cmin=None, cmax=None, cmap='viridis', **css):
    """For use with `DataFrame.style.apply` this function will apply a heatmap
    color gradient *elementwise* to the calling DataFrame

    Parameters
    ----------
    self : pd.DataFrame
        The calling DataFrame. This argument is automatically passed in by the
        `DataFrame.style.apply` method

    cvals : pd.DataFrame
        If specified this DataFrame is used to determine the color gradient

    cmin : float
        If specified, any values below this will be clipped to the bottom of
        the cmap

    cmax : float
        If specified, any values above this will be clipped to the top of
        the cmap

    cmap : colormap or str
        The colormap to use

    css : dict
        Any extra inline css key/value pars to pass to the styler

    Returns
    -------
    pd.DataFrame
        The css styles to apply

    """
    from matplotlib import cm
    from matplotlib.colors import rgb2hex
    if cvals is None:
        cvals = self.values.ravel().copy()
    else:
        assert cvals.shape == self.shape
        cvals = cvals.values.ravel().copy()
    cvals -= cmin or cvals.min()
    cvals /= cmax or cvals.max()
    cvals = cvals.clip(0, 1)
    styles = []
    for rgb in cm.viridis_r(cvals):
        style = [
            "{}: {}".format(key.replace('_', '-'), value)
            for key, value in css.items()
        ]
        style.append("background-color: {}".format(rgb2hex(rgb)))
        styles.append('; '.join(style))
    styles = np.asarray(styles).reshape(self.shape)
    return pd.DataFrame(styles, index=self.index, columns=self.columns)
```
![image](https://cloud.githubusercontent.com/assets/881019/22238182/cddb49aa-e25b-11e6-88e3-eec420793280.png)


Sounds logical, PR certainly welcome I think! 
cc @TomAugspurger 
Yep, that's be great if you could submit a PR.
this should be fairly generalized i think, iow where we accept axis

@dhirschfeld Interested to do a PR for this? (I also stumbled into a case where I would need this)

BTW, your ability to specify a vmin/vmax is also a nice feature I think.
@jorisvandenbossche  - I'd like to finish it up by submitting a PR but it's unlikely I'll have time in at least the next couple of weeks so I'd be happy for anyone else interested to pick it up. In the interim, my implementation posted above is available for anyone who finds it...

I  had a need to explicitly specify the cmin/cmax values so I implemented that directly rather than the high/low compression. I also thought the `**css` made for a very usable interactive api but concede that a case could be made that it was *too magic*

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
@@ -913,21 +913,22 @@ def background_gradient(self, cmap='PuBu', low=0, high=0, axis=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."""
+        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)
+
         with _mpl(Styler.background_gradient) as (plt, colors):
-            rng = s.max() - s.min()
+            smin = s.values.min()
+            smax = s.values.max()
+            rng = smax - smin
             # extend lower / upper bounds, compresses color range
-            norm = colors.Normalize(s.min() - (rng * low),
-                                    s.max() + (rng * high))
-            # matplotlib modifies inplace?
+            norm = colors.Normalize(smin - (rng * low), smax + (rng * high))
+            # matplotlib colors.Normalize modifies inplace?
             # https://github.com/matplotlib/matplotlib/issues/5427
-            normed = norm(s.values)
-            c = [colors.rgb2hex(x) for x in plt.cm.get_cmap(cmap)(normed)]
-            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)
+            rgbas = plt.cm.get_cmap(cmap)(norm(s.values))
 
-            def relative_luminance(color):
+            def relative_luminance(rgba):
                 """
                 Calculate relative luminance of a color.
 
@@ -936,25 +937,33 @@ def relative_luminance(color):
 
                 Parameters
                 ----------
-                color : matplotlib color
-                    Hex code, rgb-tuple, or HTML color name.
+                color : rgb or rgba tuple
 
                 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)]
+                r, g, b = (
+                    x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055 ** 2.4)
+                    for x in rgba[:3]
+                )
+                return 0.2126 * r + 0.7152 * g + 0.0722 * b
+
+            def css(rgba):
+                dark = relative_luminance(rgba) < text_color_threshold
+                text_color = '#f1f1f1' if dark else '#000000'
+                return 'background-color: {b};color: {c};'.format(
+                    b=colors.rgb2hex(rgba), c=text_color
+                )
+
+            if s.ndim == 1:
+                return [css(rgba) for rgba in rgbas]
+            else:
+                return pd.DataFrame(
+                    [[css(rgba) for rgba in row] for row in rgbas],
+                    index=s.index, columns=s.columns
+                )
 
     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
@@ -1056,6 +1056,34 @@ def test_text_color_threshold_raises(self, text_color_threshold):
             df.style.background_gradient(
                 text_color_threshold=text_color_threshold)._compute()
 
+    @td.skip_if_no_mpl
+    def test_background_gradient_axis(self):
+        df = pd.DataFrame([[1, 2], [2, 4]], columns=['A', 'B'])
+
+        low = ['background-color: #f7fbff', 'color: #000000']
+        high = ['background-color: #08306b', 'color: #f1f1f1']
+        mid = ['background-color: #abd0e6', 'color: #000000']
+        result = df.style.background_gradient(cmap='Blues',
+                                              axis=0)._compute().ctx
+        assert result[(0, 0)] == low
+        assert result[(0, 1)] == low
+        assert result[(1, 0)] == high
+        assert result[(1, 1)] == high
+
+        result = df.style.background_gradient(cmap='Blues',
+                                              axis=1)._compute().ctx
+        assert result[(0, 0)] == low
+        assert result[(0, 1)] == high
+        assert result[(1, 0)] == low
+        assert result[(1, 1)] == high
+
+        result = df.style.background_gradient(cmap='Blues',
+                                              axis=None)._compute().ctx
+        assert result[(0, 0)] == low
+        assert result[(0, 1)] == mid
+        assert result[(1, 0)] == mid
+        assert result[(1, 1)] == high
+
 
 def test_block_names():
     # catch accidental removal of a block