43
43
from pandas .api .types import is_list_like
44
44
from pandas .core import generic
45
45
import pandas .core .common as com
46
- from pandas .core .frame import DataFrame
46
+ from pandas .core .frame import (
47
+ DataFrame ,
48
+ Series ,
49
+ )
47
50
from pandas .core .generic import NDFrame
48
51
from pandas .core .indexes .api import Index
49
52
@@ -179,7 +182,7 @@ def __init__(
179
182
escape : bool = False ,
180
183
):
181
184
# validate ordered args
182
- if isinstance (data , pd . Series ):
185
+ if isinstance (data , Series ):
183
186
data = data .to_frame ()
184
187
if not isinstance (data , DataFrame ):
185
188
raise TypeError ("``data`` must be a Series or DataFrame" )
@@ -1438,67 +1441,136 @@ def background_gradient(
1438
1441
text_color_threshold : float = 0.408 ,
1439
1442
vmin : float | None = None ,
1440
1443
vmax : float | None = None ,
1444
+ gmap : Sequence | None = None ,
1441
1445
) -> Styler :
1442
1446
"""
1443
1447
Color the background in a gradient style.
1444
1448
1445
1449
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.
1447
1452
1448
1453
Parameters
1449
1454
----------
1450
1455
cmap : str or colormap
1451
1456
Matplotlib colormap.
1452
1457
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.
1454
1461
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.
1456
1465
axis : {0 or 'index', 1 or 'columns', None}, default 0
1457
1466
Apply to each column (``axis=0`` or ``'index'``), to each row
1458
1467
(``axis=1`` or ``'columns'``), or to the entire DataFrame at once
1459
1468
with ``axis=None``.
1460
1469
subset : IndexSlice
1461
1470
A valid slice for ``data`` to limit the style application to.
1462
1471
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 .
1466
1475
1467
1476
.. versionadded:: 0.24.0
1468
1477
1469
1478
vmin : float, optional
1470
1479
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.
1472
1481
1473
1482
.. versionadded:: 1.0.0
1474
1483
1475
1484
vmax : float, optional
1476
1485
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.
1478
1487
1479
1488
.. versionadded:: 1.0.0
1480
1489
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
+
1481
1501
Returns
1482
1502
-------
1483
1503
self : Styler
1484
1504
1485
- Raises
1486
- ------
1487
- ValueError
1488
- If ``text_color_threshold`` is not a value from 0 to 1.
1489
-
1490
1505
Notes
1491
1506
-----
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
1496
1568
"""
1497
- if subset is None :
1569
+ if subset is None and gmap is None :
1498
1570
subset = self .data .select_dtypes (include = np .number ).columns
1499
1571
1500
1572
self .apply (
1501
- self . _background_gradient ,
1573
+ _background_gradient ,
1502
1574
cmap = cmap ,
1503
1575
subset = subset ,
1504
1576
axis = axis ,
@@ -1507,75 +1579,10 @@ def background_gradient(
1507
1579
text_color_threshold = text_color_threshold ,
1508
1580
vmin = vmin ,
1509
1581
vmax = vmax ,
1582
+ gmap = gmap ,
1510
1583
)
1511
1584
return self
1512
1585
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
-
1579
1586
def set_properties (self , subset = None , ** kwargs ) -> Styler :
1580
1587
"""
1581
1588
Set defined CSS-properties to each ``<td>`` HTML element within the given
@@ -2346,3 +2353,119 @@ def pred(part) -> bool:
2346
2353
else :
2347
2354
slice_ = [part if pred (part ) else [part ] for part in slice_ ]
2348
2355
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