4343from pandas .api .types import is_list_like
4444from pandas .core import generic
4545import pandas .core .common as com
46- from pandas .core .frame import DataFrame
46+ from pandas .core .frame import (
47+ DataFrame ,
48+ Series ,
49+ )
4750from pandas .core .generic import NDFrame
4851from pandas .core .indexes .api import Index
4952
@@ -179,7 +182,7 @@ def __init__(
179182 escape : bool = False ,
180183 ):
181184 # validate ordered args
182- if isinstance (data , pd . Series ):
185+ if isinstance (data , Series ):
183186 data = data .to_frame ()
184187 if not isinstance (data , DataFrame ):
185188 raise TypeError ("``data`` must be a Series or DataFrame" )
@@ -1438,67 +1441,136 @@ def background_gradient(
14381441 text_color_threshold : float = 0.408 ,
14391442 vmin : float | None = None ,
14401443 vmax : float | None = None ,
1444+ gmap : Sequence | None = None ,
14411445 ) -> Styler :
14421446 """
14431447 Color the background in a gradient style.
14441448
14451449 The background color is determined according
1446- to the data in each column (optionally row). Requires matplotlib.
1450+ to the data in each column, row or frame, or by a given
1451+ gradient map. Requires matplotlib.
14471452
14481453 Parameters
14491454 ----------
14501455 cmap : str or colormap
14511456 Matplotlib colormap.
14521457 low : float
1453- Compress the range by the low.
1458+ Compress the color range at the low end. This is a multiple of the data
1459+ range to extend below the minimum; good values usually in [0, 1],
1460+ defaults to 0.
14541461 high : float
1455- Compress the range by the high.
1462+ Compress the color range at the high end. This is a multiple of the data
1463+ range to extend above the maximum; good values usually in [0, 1],
1464+ defaults to 0.
14561465 axis : {0 or 'index', 1 or 'columns', None}, default 0
14571466 Apply to each column (``axis=0`` or ``'index'``), to each row
14581467 (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
14591468 with ``axis=None``.
14601469 subset : IndexSlice
14611470 A valid slice for ``data`` to limit the style application to.
14621471 text_color_threshold : float or int
1463- Luminance threshold for determining text color. Facilitates text
1464- visibility across varying background colors. From 0 to 1.
1465- 0 = all text is dark colored, 1 = all text is light colored .
1472+ Luminance threshold for determining text color in [0, 1] . Facilitates text
1473+ visibility across varying background colors. All text is dark if 0, and
1474+ light if 1, defaults to 0.408 .
14661475
14671476 .. versionadded:: 0.24.0
14681477
14691478 vmin : float, optional
14701479 Minimum data value that corresponds to colormap minimum value.
1471- When None (default): the minimum value of the data will be used.
1480+ If not specified the minimum value of the data (or gmap) will be used.
14721481
14731482 .. versionadded:: 1.0.0
14741483
14751484 vmax : float, optional
14761485 Maximum data value that corresponds to colormap maximum value.
1477- When None (default): the maximum value of the data will be used.
1486+ If not specified the maximum value of the data (or gmap) will be used.
14781487
14791488 .. versionadded:: 1.0.0
14801489
1490+ gmap : array-like, optional
1491+ Gradient map for determining the background colors. If not supplied
1492+ will use the underlying data from rows, columns or frame. If given as an
1493+ ndarray or list-like must be an identical shape to the underlying data
1494+ considering ``axis`` and ``subset``. If given as DataFrame or Series must
1495+ have same index and column labels considering ``axis`` and ``subset``.
1496+ If supplied, ``vmin`` and ``vmax`` should be given relative to this
1497+ gradient map.
1498+
1499+ .. versionadded:: 1.3.0
1500+
14811501 Returns
14821502 -------
14831503 self : Styler
14841504
1485- Raises
1486- ------
1487- ValueError
1488- If ``text_color_threshold`` is not a value from 0 to 1.
1489-
14901505 Notes
14911506 -----
1492- Set ``text_color_threshold`` or tune ``low`` and ``high`` to keep the
1493- text legible by not using the entire range of the color map. The range
1494- of the data is extended by ``low * (x.max() - x.min())`` and ``high *
1495- (x.max() - x.min())`` before normalizing.
1507+ When using ``low`` and ``high`` the range
1508+ of the gradient, given by the data if ``gmap`` is not given or by ``gmap``,
1509+ is extended at the low end effectively by
1510+ `map.min - low * map.range` and at the high end by
1511+ `map.max + high * map.range` before the colors are normalized and determined.
1512+
1513+ If combining with ``vmin`` and ``vmax`` the `map.min`, `map.max` and
1514+ `map.range` are replaced by values according to the values derived from
1515+ ``vmin`` and ``vmax``.
1516+
1517+ This method will preselect numeric columns and ignore non-numeric columns
1518+ unless a ``gmap`` is supplied in which case no preselection occurs.
1519+
1520+ Examples
1521+ --------
1522+ >>> df = pd.DataFrame({
1523+ ... 'City': ['Stockholm', 'Oslo', 'Copenhagen'],
1524+ ... 'Temp (c)': [21.6, 22.4, 24.5],
1525+ ... 'Rain (mm)': [5.0, 13.3, 0.0],
1526+ ... 'Wind (m/s)': [3.2, 3.1, 6.7]
1527+ ... })
1528+
1529+ Shading the values column-wise, with ``axis=0``, preselecting numeric columns
1530+
1531+ >>> df.style.background_gradient(axis=0)
1532+
1533+ .. figure:: ../../_static/style/bg_ax0.png
1534+
1535+ Shading all values collectively using ``axis=None``
1536+
1537+ >>> df.style.background_gradient(axis=None)
1538+
1539+ .. figure:: ../../_static/style/bg_axNone.png
1540+
1541+ Compress the color map from the both ``low`` and ``high`` ends
1542+
1543+ >>> df.style.background_gradient(axis=None, low=0.75, high=1.0)
1544+
1545+ .. figure:: ../../_static/style/bg_axNone_lowhigh.png
1546+
1547+ Manually setting ``vmin`` and ``vmax`` gradient thresholds
1548+
1549+ >>> df.style.background_gradient(axis=None, vmin=6.7, vmax=21.6)
1550+
1551+ .. figure:: ../../_static/style/bg_axNone_vminvmax.png
1552+
1553+ Setting a ``gmap`` and applying to all columns with another ``cmap``
1554+
1555+ >>> df.style.background_gradient(axis=0, gmap=df['Temp (c)'], cmap='YlOrRd')
1556+
1557+ .. figure:: ../../_static/style/bg_gmap.png
1558+
1559+ Setting the gradient map for a dataframe (i.e. ``axis=None``), we need to
1560+ explicitly state ``subset`` to match the ``gmap`` shape
1561+
1562+ >>> gmap = np.array([[1,2,3], [2,3,4], [3,4,5]])
1563+ >>> df.style.background_gradient(axis=None, gmap=gmap,
1564+ ... cmap='YlOrRd', subset=['Temp (c)', 'Rain (mm)', 'Wind (m/s)']
1565+ ... )
1566+
1567+ .. figure:: ../../_static/style/bg_axNone_gmap.png
14961568 """
1497- if subset is None :
1569+ if subset is None and gmap is None :
14981570 subset = self .data .select_dtypes (include = np .number ).columns
14991571
15001572 self .apply (
1501- self . _background_gradient ,
1573+ _background_gradient ,
15021574 cmap = cmap ,
15031575 subset = subset ,
15041576 axis = axis ,
@@ -1507,75 +1579,10 @@ def background_gradient(
15071579 text_color_threshold = text_color_threshold ,
15081580 vmin = vmin ,
15091581 vmax = vmax ,
1582+ gmap = gmap ,
15101583 )
15111584 return self
15121585
1513- @staticmethod
1514- def _background_gradient (
1515- s ,
1516- cmap = "PuBu" ,
1517- low : float = 0 ,
1518- high : float = 0 ,
1519- text_color_threshold : float = 0.408 ,
1520- vmin : float | None = None ,
1521- vmax : float | None = None ,
1522- ):
1523- """
1524- Color background in a range according to the data.
1525- """
1526- if (
1527- not isinstance (text_color_threshold , (float , int ))
1528- or not 0 <= text_color_threshold <= 1
1529- ):
1530- msg = "`text_color_threshold` must be a value from 0 to 1."
1531- raise ValueError (msg )
1532-
1533- with _mpl (Styler .background_gradient ) as (plt , colors ):
1534- smin = np .nanmin (s .to_numpy ()) if vmin is None else vmin
1535- smax = np .nanmax (s .to_numpy ()) if vmax is None else vmax
1536- rng = smax - smin
1537- # extend lower / upper bounds, compresses color range
1538- norm = colors .Normalize (smin - (rng * low ), smax + (rng * high ))
1539- # matplotlib colors.Normalize modifies inplace?
1540- # https://github.com/matplotlib/matplotlib/issues/5427
1541- rgbas = plt .cm .get_cmap (cmap )(norm (s .to_numpy (dtype = float )))
1542-
1543- def relative_luminance (rgba ) -> float :
1544- """
1545- Calculate relative luminance of a color.
1546-
1547- The calculation adheres to the W3C standards
1548- (https://www.w3.org/WAI/GL/wiki/Relative_luminance)
1549-
1550- Parameters
1551- ----------
1552- color : rgb or rgba tuple
1553-
1554- Returns
1555- -------
1556- float
1557- The relative luminance as a value from 0 to 1
1558- """
1559- r , g , b = (
1560- x / 12.92 if x <= 0.04045 else ((x + 0.055 ) / 1.055 ) ** 2.4
1561- for x in rgba [:3 ]
1562- )
1563- return 0.2126 * r + 0.7152 * g + 0.0722 * b
1564-
1565- def css (rgba ) -> str :
1566- dark = relative_luminance (rgba ) < text_color_threshold
1567- text_color = "#f1f1f1" if dark else "#000000"
1568- return f"background-color: { colors .rgb2hex (rgba )} ;color: { text_color } ;"
1569-
1570- if s .ndim == 1 :
1571- return [css (rgba ) for rgba in rgbas ]
1572- else :
1573- return DataFrame (
1574- [[css (rgba ) for rgba in row ] for row in rgbas ],
1575- index = s .index ,
1576- columns = s .columns ,
1577- )
1578-
15791586 def set_properties (self , subset = None , ** kwargs ) -> Styler :
15801587 """
15811588 Set defined CSS-properties to each ``<td>`` HTML element within the given
@@ -2346,3 +2353,119 @@ def pred(part) -> bool:
23462353 else :
23472354 slice_ = [part if pred (part ) else [part ] for part in slice_ ]
23482355 return tuple (slice_ )
2356+
2357+
2358+ def _validate_apply_axis_arg (
2359+ arg : FrameOrSeries | Sequence | np .ndarray ,
2360+ arg_name : str ,
2361+ dtype : Any | None ,
2362+ data : FrameOrSeries ,
2363+ ) -> np .ndarray :
2364+ """
2365+ For the apply-type methods, ``axis=None`` creates ``data`` as DataFrame, and for
2366+ ``axis=[1,0]`` it creates a Series. Where ``arg`` is expected as an element
2367+ of some operator with ``data`` we must make sure that the two are compatible shapes,
2368+ or raise.
2369+
2370+ Parameters
2371+ ----------
2372+ arg : sequence, Series or DataFrame
2373+ the user input arg
2374+ arg_name : string
2375+ name of the arg for use in error messages
2376+ dtype : numpy dtype, optional
2377+ forced numpy dtype if given
2378+ data : Series or DataFrame
2379+ underling subset of Styler data on which operations are performed
2380+
2381+ Returns
2382+ -------
2383+ ndarray
2384+ """
2385+ dtype = {"dtype" : dtype } if dtype else {}
2386+ # raise if input is wrong for axis:
2387+ if isinstance (arg , Series ) and isinstance (data , DataFrame ):
2388+ raise ValueError (
2389+ f"'{ arg_name } ' is a Series but underlying data for operations "
2390+ f"is a DataFrame since 'axis=None'"
2391+ )
2392+ elif isinstance (arg , DataFrame ) and isinstance (data , Series ):
2393+ raise ValueError (
2394+ f"'{ arg_name } ' is a DataFrame but underlying data for "
2395+ f"operations is a Series with 'axis in [0,1]'"
2396+ )
2397+ elif isinstance (arg , (Series , DataFrame )): # align indx / cols to data
2398+ arg = arg .reindex_like (data , method = None ).to_numpy (** dtype )
2399+ else :
2400+ arg = np .asarray (arg , ** dtype )
2401+ assert isinstance (arg , np .ndarray ) # mypy requirement
2402+ if arg .shape != data .shape : # check valid input
2403+ raise ValueError (
2404+ f"supplied '{ arg_name } ' is not correct shape for data over "
2405+ f"selected 'axis': got { arg .shape } , "
2406+ f"expected { data .shape } "
2407+ )
2408+ return arg
2409+
2410+
2411+ def _background_gradient (
2412+ data ,
2413+ cmap = "PuBu" ,
2414+ low : float = 0 ,
2415+ high : float = 0 ,
2416+ text_color_threshold : float = 0.408 ,
2417+ vmin : float | None = None ,
2418+ vmax : float | None = None ,
2419+ gmap : Sequence | np .ndarray | FrameOrSeries | None = None ,
2420+ ):
2421+ """
2422+ Color background in a range according to the data or a gradient map
2423+ """
2424+ if gmap is None : # the data is used the gmap
2425+ gmap = data .to_numpy (dtype = float )
2426+ else : # else validate gmap against the underlying data
2427+ gmap = _validate_apply_axis_arg (gmap , "gmap" , float , data )
2428+
2429+ with _mpl (Styler .background_gradient ) as (plt , colors ):
2430+ smin = np .nanmin (gmap ) if vmin is None else vmin
2431+ smax = np .nanmax (gmap ) if vmax is None else vmax
2432+ rng = smax - smin
2433+ # extend lower / upper bounds, compresses color range
2434+ norm = colors .Normalize (smin - (rng * low ), smax + (rng * high ))
2435+ rgbas = plt .cm .get_cmap (cmap )(norm (gmap ))
2436+
2437+ def relative_luminance (rgba ) -> float :
2438+ """
2439+ Calculate relative luminance of a color.
2440+
2441+ The calculation adheres to the W3C standards
2442+ (https://www.w3.org/WAI/GL/wiki/Relative_luminance)
2443+
2444+ Parameters
2445+ ----------
2446+ color : rgb or rgba tuple
2447+
2448+ Returns
2449+ -------
2450+ float
2451+ The relative luminance as a value from 0 to 1
2452+ """
2453+ r , g , b = (
2454+ x / 12.92 if x <= 0.04045 else ((x + 0.055 ) / 1.055 ) ** 2.4
2455+ for x in rgba [:3 ]
2456+ )
2457+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
2458+
2459+ def css (rgba ) -> str :
2460+ dark = relative_luminance (rgba ) < text_color_threshold
2461+ text_color = "#f1f1f1" if dark else "#000000"
2462+ return f"background-color: { colors .rgb2hex (rgba )} ;color: { text_color } ;"
2463+
2464+ if data .ndim == 1 :
2465+ return [css (rgba ) for rgba in rgbas ]
2466+ else :
2467+ return DataFrame (
2468+ [[css (rgba ) for rgba in row ] for row in rgbas ],
2469+ index = data .index ,
2470+ columns = data .columns ,
2471+ )
0 commit comments