Perform data cell colorization.
GT.data_color(
columns=None,
rows=None,
palette=None,
domain=None,
midpoint=None,
na_color=None,
alpha=None,
reverse=False,
autocolor_text=True,
contrast_algo="apca",
truncate=False,
fn=None,
direction="column",
)
It’s possible to add color to data cells according to their values with the data_color() method. There is a multitude of ways to perform data cell colorizing here:
- targeting: we can constrain which columns should receive the colorization treatment through the
columns= argument)
- color palettes: with
palette= we could supply a list of colors composed of hexadecimal values or color names
- value domain: we can either opt to have the range of values define the domain, or, specify one explicitly with the
domain= argument
- color-mapping function: for complete control over the mapping of values to colors, a function can be supplied to
fn=
- text autocoloring: data_color() will automatically recolor the foreground text to provide the best contrast (can be deactivated with
autocolor_text=False)
Parameters
columns: SelectExpr = None
-
The columns to target. Can either be a single column name or a series of column names provided in a list.
rows: RowSelectExpr = None
-
In conjunction with columns=, we can specify which rows should be colored. By default, all rows in the targeted columns will be colored. Alternatively, we can provide a list of row indices.
palette: str | list[str] | None = None
-
The color palette to use. This should be a list of colors (e.g., ["#FF0000", "#00FF00", "#0000FF"]). A ColorBrewer palette could also be used, just supply the name (reference available in the Color palette access from ColorBrewer section). If None, then a default palette will be used.
domain: list[str] | list[int] | list[float] | None = None
-
The domain of values to use for the color scheme. This can be a list of floats, integers, or strings. If None, then the domain will be inferred from the data values (see the How column values are mapped to colors section for details).
midpoint: int | float | None = None
-
A value to center the color scale on, for numeric columns (e.g., 0 for values that represent a change). The midpoint receives the color at the center of the palette, which makes this most useful with a diverging palette like ["red", "white", "green"] or "RdBu". If domain= is None, then the domain is made symmetric around the midpoint so that values equally far from it on either side are equally strong in color. If domain= is supplied, then each side of the midpoint is scaled separately to its end of the domain (and the midpoint must lie within the domain). See the Centering colors on a midpoint section for details.
na_color: str | None = None
-
The color to use for missing values. If None, then the default color ("#808080") will be used.
alpha: float | None = None
-
An optional, fixed alpha transparency value that will be applied to all color palette values.
reverse: bool = False
-
Should the colors computed operate in the reverse order? If True then colors that normally change from red to blue will change in the opposite direction.
autocolor_text: bool = True
-
Whether or not to automatically color the text of the data values. If True, then the text will be colored according to the background color of the cell.
contrast_algo: ContrastAlgo = "apca"
-
The color contrast algorithm to use when autocolor_text=True. By default this is "apca" (Accessible Perceptual Contrast Algorithm) and the alternative to this is "wcag" (Web Content Accessibility Guidelines). The chosen algorithm determines whether light or dark text provides better contrast against each cell’s background color.
truncate: bool = False
-
If True, then any values that fall outside of the domain will be truncated to the minimum or maximum value of the domain (will have the same color). If False, then any values that fall outside of the domain will be set to NaN and will follow the na_color= color.
fn: Callable[[list[Any]], list[str | None]] | None = None
-
A color-mapping function. The function should take a list of data values (from a single column, or from a single row with direction="row") as input and return a list of colors of the same length. Each color may be a hexadecimal color value (e.g., "#FF0000") or a color name (e.g., "red"). All values are passed to the function, including missing values, so the function must handle those itself. Any position where the function returns None receives the na_color= color. If a function is supplied, then the palette=, domain=, reverse=, truncate=, and midpoint= arguments are ignored. The alpha=, autocolor_text=, and contrast_algo= arguments still apply to the returned colors. The col_numeric(), col_bin(), and col_factor() helpers create ready-made color-mapping functions that can be supplied here.
direction: Literal["column", "row"] = "column"
-
Should the color computations be performed column-wise or row-wise? By default this is
"column" and the colors are computed down each targeted column. With "row", the colors are computed across each targeted row instead, using the values of that row in the targeted columns (so each row has its own domain when the domain is inferred).
Returns
GT
-
The GT object is returned. This is the same object that the method is called on so that we can facilitate method chaining.
How Column Values Are Mapped To Colors
Without fn=, each targeted column is mapped to colors in one of two ways, depending on the values it contains (missing values are set aside first):
- numeric values (integers, floats, and
Decimal values) are spread along the palette as a continuous gradient, from the lowest value in the domain to the highest
- string values (including categorical columns) are treated as categories, with each one taking its own position along the palette
Columns of any other type (e.g., dates) or with a mix of types will raise an error. To color these, supply a color-mapping function to fn= (see the Custom color-mapping functions section). Some further details on how values are handled:
- missing values (
None, NaN, and pd.NA) always receive the na_color= color
- booleans are treated as numbers, so
True takes the high end of the palette and False the low end
- an inferred numeric domain spans the finite values only; any infinite values are given the color at the matching end of the palette (with a supplied
domain=, infinite values are treated like any other value outside of the domain, following the truncate= setting)
- an inferred categorical domain lists the categories in the order they first appear in the column, except for ordered categorical columns (an ordered Pandas
Categorical or a Polars Enum), where the declared levels are used in their declared order (this includes any levels that don’t appear in the data, so colors stay consistent no matter which rows= are colored)
Supplying domain= overrides any of the inferred domains described above.
With direction="row", all of this applies to the values of each targeted row (across the targeted columns) instead of to each column. An inferred categorical domain then lists the categories in the order they first appear in the row.
Centering Colors On A Midpoint
For numeric values that are naturally centered on some value (changes centered on 0, ratios centered on 1, scores centered on a target), the midpoint= argument pins that value to the center color of the palette. How the rest of the values map to colors depends on whether domain= is supplied. As an example, suppose a column spans from -2 to 10 and we use palette=["red", "white", "green"] with midpoint=0:
- without
domain=, the domain becomes symmetric around the midpoint (here, [-10, 10]), so 10 is fully green while -2 is only a pale red: the strength of a color reflects the distance from the midpoint, the same on both sides
- with
domain=[-2, 10], each side of the midpoint is scaled separately, so -2 is fully red and 10 is fully green (this is how three-color scales work in spreadsheet software, where both sides always use their full range of colors)
For finer control (such as pinning every palette color to a specific value, or to a percentage of the way through the range of values), use fn=col_numeric(stops=...) (see col_numeric()).
Color Palette Access From Colorbrewer And Viridis
All palettes from the ColorBrewer package can be accessed by providing the palette name in palette=. There are 35 available palettes:
| 1 |
"BrBG" |
11 |
Diverging |
Yes |
| 2 |
"PiYG" |
11 |
Diverging |
Yes |
| 3 |
"PRGn" |
11 |
Diverging |
Yes |
| 4 |
"PuOr" |
11 |
Diverging |
Yes |
| 5 |
"RdBu" |
11 |
Diverging |
Yes |
| 6 |
"RdYlBu" |
11 |
Diverging |
Yes |
| 7 |
"RdGy" |
11 |
Diverging |
No |
| 8 |
"RdYlGn" |
11 |
Diverging |
No |
| 9 |
"Spectral" |
11 |
Diverging |
No |
| 10 |
"Dark2" |
8 |
Qualitative |
Yes |
| 11 |
"Paired" |
12 |
Qualitative |
Yes |
| 12 |
"Set1" |
9 |
Qualitative |
No |
| 13 |
"Set2" |
8 |
Qualitative |
Yes |
| 14 |
"Set3" |
12 |
Qualitative |
No |
| 15 |
"Accent" |
8 |
Qualitative |
No |
| 16 |
"Pastel1" |
9 |
Qualitative |
No |
| 17 |
"Pastel2" |
8 |
Qualitative |
No |
| 18 |
"Blues" |
9 |
Sequential |
Yes |
| 19 |
"BuGn" |
9 |
Sequential |
Yes |
| 20 |
"BuPu" |
9 |
Sequential |
Yes |
| 21 |
"GnBu" |
9 |
Sequential |
Yes |
| 22 |
"Greens" |
9 |
Sequential |
Yes |
| 23 |
"Greys" |
9 |
Sequential |
Yes |
| 24 |
"Oranges" |
9 |
Sequential |
Yes |
| 25 |
"OrRd" |
9 |
Sequential |
Yes |
| 26 |
"PuBu" |
9 |
Sequential |
Yes |
| 27 |
"PuBuGn" |
9 |
Sequential |
Yes |
| 28 |
"PuRd" |
9 |
Sequential |
Yes |
| 29 |
"Purples" |
9 |
Sequential |
Yes |
| 30 |
"RdPu" |
9 |
Sequential |
Yes |
| 31 |
"Reds" |
9 |
Sequential |
Yes |
| 32 |
"YlGn" |
9 |
Sequential |
Yes |
| 33 |
"YlGnBu" |
9 |
Sequential |
Yes |
| 34 |
"YlOrBr" |
9 |
Sequential |
Yes |
| 35 |
"YlOrRd" |
9 |
Sequential |
Yes |
We can also use the viridis and associated color palettes by providing to palette= any of the following string values: "viridis", "plasma", "inferno", "magma", or "cividis".
Custom Color-Mapping Functions
When the built-in mapping (a palette spread across a domain) isn’t what you need, you can supply your own color-mapping function to fn=. This gives complete control over how values become colors. Here’s how such a function is used:
- it’s called once per targeted column, receiving a list of that column’s values (only for the rows selected by
rows=, in table order); with direction="row", it’s called once per targeted row instead, receiving that row’s values in the targeted columns
- it must return a list of colors of the same length, where each color is a hexadecimal value (
"#RRGGBB", "#RRGGBBAA", or the short "#RGB" form) or a CSS/X11 color name
- missing values are included in the list, so the function must handle them: with pandas they usually arrive as
NaN (or None/pd.NA, depending on the column type), and with Polars and PyArrow they arrive as None; pd.isna() catches all of these
- returning
None at any position gives that cell the na_color= color, which is the easiest way to deal with missing values
- columns of any type can be colored (e.g., booleans or dates), since the numeric-or-string requirement of the built-in mapping doesn’t apply
When fn= is used, the palette=, domain=, reverse=, truncate=, and midpoint= arguments are ignored because the function takes over their roles. The na_color=, alpha=, autocolor_text=, and contrast_algo= arguments still apply to the colors that the function returns.
Rather than writing a function from scratch, you can create one with these helpers (modeled on the color-mapping functions of the R scales package, which are commonly used with gt):
col_numeric(): a continuous gradient across a numeric domain, optionally with each palette color pinned to a specific value (or a percentage of the way through the range of values) with stops=
col_bin(): numeric values cut into bins, with one color per bin
col_factor(): one color per category, optionally with a fixed set of levels so colors stay consistent across tables
Each helper accepts a palette in the same forms as palette= here (including ColorBrewer and viridis palette names). The numeric helpers also have their own domain= and truncate= arguments. Missing values, and values outside of the domain, give None by default, so the na_color= value given to data_color() is used for those cells. Because the helpers return ordinary functions, they can also be called inside your own function, for instance to compute a domain from the data before mapping it.
Examples
The data_color() method can be used without any supplied arguments to colorize a table. Let’s do this with the exibble dataset:
from great_tables import GT
from great_tables.data import exibble
GT(exibble).data_color()
| num |
char |
fctr |
date |
time |
datetime |
currency |
row |
group |
| 0.1111 |
apricot |
one |
2015-01-15 |
13:35 |
2018-01-01 02:22 |
49.95 |
row_1 |
grp_a |
| 2.222 |
banana |
two |
2015-02-15 |
14:40 |
2018-02-02 14:33 |
17.95 |
row_2 |
grp_a |
| 33.33 |
coconut |
three |
2015-03-15 |
15:45 |
2018-03-03 03:44 |
1.39 |
row_3 |
grp_a |
| 444.4 |
durian |
four |
2015-04-15 |
16:50 |
2018-04-04 15:55 |
65100.0 |
row_4 |
grp_a |
| 5550.0 |
None |
five |
2015-05-15 |
17:55 |
2018-05-05 04:00 |
1325.81 |
row_5 |
grp_b |
| None |
fig |
six |
2015-06-15 |
None |
2018-06-06 16:11 |
13.255 |
row_6 |
grp_b |
| 777000.0 |
grapefruit |
seven |
None |
19:10 |
2018-07-07 05:22 |
None |
row_7 |
grp_b |
| 8880000.0 |
honeydew |
eight |
2015-08-15 |
20:20 |
None |
0.44 |
row_8 |
grp_b |
What’s happened is that data_color() applies background colors to all cells of every column with the palette of eight colors. Numeric columns will use ‘numeric’ methodology for color scaling whereas string-based columns will use the ‘factor’ methodology. The text color undergoes an automatic modification that maximizes contrast (since autocolor_text=True by default).
We can target specific colors and apply color to just those columns. Let’s do that and also supply palette= values of "red" and "green".
GT(exibble).data_color(
columns=["num", "currency"],
palette=["red", "green"]
)
| num |
char |
fctr |
date |
time |
datetime |
currency |
row |
group |
| 0.1111 |
apricot |
one |
2015-01-15 |
13:35 |
2018-01-01 02:22 |
49.95 |
row_1 |
grp_a |
| 2.222 |
banana |
two |
2015-02-15 |
14:40 |
2018-02-02 14:33 |
17.95 |
row_2 |
grp_a |
| 33.33 |
coconut |
three |
2015-03-15 |
15:45 |
2018-03-03 03:44 |
1.39 |
row_3 |
grp_a |
| 444.4 |
durian |
four |
2015-04-15 |
16:50 |
2018-04-04 15:55 |
65100.0 |
row_4 |
grp_a |
| 5550.0 |
None |
five |
2015-05-15 |
17:55 |
2018-05-05 04:00 |
1325.81 |
row_5 |
grp_b |
| None |
fig |
six |
2015-06-15 |
None |
2018-06-06 16:11 |
13.255 |
row_6 |
grp_b |
| 777000.0 |
grapefruit |
seven |
None |
19:10 |
2018-07-07 05:22 |
None |
row_7 |
grp_b |
| 8880000.0 |
honeydew |
eight |
2015-08-15 |
20:20 |
None |
0.44 |
row_8 |
grp_b |
With those options in place we see that only the numeric columns num and currency received color treatments. Moreover, the palette colors were mapped to the lower and upper limits of the data in each column; interpolated colors were used for the values in between the numeric limits of the two columns.
We can manually set the limits of the data with the domain= argument (which is preferable in most cases). Let’s colorize just the currency column and set domain=[0, 50]. Any values that are either missing or lie outside of the domain will be colorized with the na_color= color (so we’ll set that to "lightgray").
GT(exibble).data_color(
columns="currency",
palette=["red", "green"],
domain=[0, 50],
na_color="lightgray"
)
| num |
char |
fctr |
date |
time |
datetime |
currency |
row |
group |
| 0.1111 |
apricot |
one |
2015-01-15 |
13:35 |
2018-01-01 02:22 |
49.95 |
row_1 |
grp_a |
| 2.222 |
banana |
two |
2015-02-15 |
14:40 |
2018-02-02 14:33 |
17.95 |
row_2 |
grp_a |
| 33.33 |
coconut |
three |
2015-03-15 |
15:45 |
2018-03-03 03:44 |
1.39 |
row_3 |
grp_a |
| 444.4 |
durian |
four |
2015-04-15 |
16:50 |
2018-04-04 15:55 |
65100.0 |
row_4 |
grp_a |
| 5550.0 |
None |
five |
2015-05-15 |
17:55 |
2018-05-05 04:00 |
1325.81 |
row_5 |
grp_b |
| None |
fig |
six |
2015-06-15 |
None |
2018-06-06 16:11 |
13.255 |
row_6 |
grp_b |
| 777000.0 |
grapefruit |
seven |
None |
19:10 |
2018-07-07 05:22 |
None |
row_7 |
grp_b |
| 8880000.0 |
honeydew |
eight |
2015-08-15 |
20:20 |
None |
0.44 |
row_8 |
grp_b |
For complete control over how values map to colors, we can supply a function to fn=. The function receives the list of values in a column (missing values included) and must return a list of colors of the same length. Here, values in the num column are colored according to whether they are below or above 100. Returning None for the missing value means that it gets the na_color= color:
import pandas as pd
def above_below_100(vals):
return [None if pd.isna(x) else "lightblue" if x < 100 else "orange" for x in vals]
GT(exibble).data_color(
columns="num",
fn=above_below_100,
na_color="lightgray"
)
| num |
char |
fctr |
date |
time |
datetime |
currency |
row |
group |
| 0.1111 |
apricot |
one |
2015-01-15 |
13:35 |
2018-01-01 02:22 |
49.95 |
row_1 |
grp_a |
| 2.222 |
banana |
two |
2015-02-15 |
14:40 |
2018-02-02 14:33 |
17.95 |
row_2 |
grp_a |
| 33.33 |
coconut |
three |
2015-03-15 |
15:45 |
2018-03-03 03:44 |
1.39 |
row_3 |
grp_a |
| 444.4 |
durian |
four |
2015-04-15 |
16:50 |
2018-04-04 15:55 |
65100.0 |
row_4 |
grp_a |
| 5550.0 |
None |
five |
2015-05-15 |
17:55 |
2018-05-05 04:00 |
1325.81 |
row_5 |
grp_b |
| None |
fig |
six |
2015-06-15 |
None |
2018-06-06 16:11 |
13.255 |
row_6 |
grp_b |
| 777000.0 |
grapefruit |
seven |
None |
19:10 |
2018-07-07 05:22 |
None |
row_7 |
grp_b |
| 8880000.0 |
honeydew |
eight |
2015-08-15 |
20:20 |
None |
0.44 |
row_8 |
grp_b |
A color-mapping function is also useful for highlighting the sign of values. Here, positive changes are colored green, negative changes orange, and zero values are left white. Because the function assigns its own color to the missing value, na_color= isn’t needed:
df = pd.DataFrame(
{
"region": ["North", "South", "East", "West", "Central"],
"change": [12.5, -8.1, 0.0, 3.2, None],
}
)
def sign_colors(vals):
return [
"#E0E0E0" if pd.isna(x) else "#1B9E77" if x > 0 else "#D95F02" if x < 0 else "#FFFFFF"
for x in vals
]
(
GT(df)
.data_color(columns="change", fn=sign_colors)
.fmt_number(columns="change", decimals=1, force_sign=True)
)
| region |
change |
| North |
+12.5 |
| South |
−8.1 |
| East |
0.0 |
| West |
+3.2 |
| Central |
None |
For a gradient that’s centered on zero, rather than fixed colors, we can use midpoint=0 with a diverging palette. Negative changes become increasingly red and positive changes increasingly green, the further they are from zero. Because no domain= is given, the domain is made symmetric around zero, so the largest decrease (-8.1) is a less intense red than the largest increase (12.5) is green:
(
GT(df)
.data_color(columns="change", palette=["#D7191C", "white", "#1A9641"], midpoint=0)
.fmt_number(columns="change", decimals=1, force_sign=True)
)
| region |
change |
| North |
+12.5 |
| South |
−8.1 |
| East |
0.0 |
| West |
+3.2 |
| Central |
None |
The midpoint doesn’t have to be zero, and it doesn’t have to sit in the middle of the domain. Here, sales are shown as a fraction of a target, so the midpoint is 1. Supplying a domain= means that each side of the midpoint is scaled separately: the colors go from red to white over the wide range from 50% of the target up to the target, and from white to green over the narrower range from the target up to 120% of it:
sales_df = pd.DataFrame(
{
"rep": ["Ana", "Ben", "Cai", "Dee", "Eli"],
"pct_of_target": [0.62, 0.88, 1.0, 1.08, 1.17],
}
)
(
GT(sales_df)
.data_color(
columns="pct_of_target",
palette=["#D7191C", "white", "#1A9641"],
domain=[0.5, 1.2],
midpoint=1,
)
.fmt_percent(columns="pct_of_target", decimals=0)
)
| rep |
pct_of_target |
| Ana |
62% |
| Ben |
88% |
| Cai |
100% |
| Dee |
108% |
| Eli |
117% |
Colors are computed down each column by default. With direction="row", they’re computed across each row instead, which is useful when the values within a row are comparable but the rows are on different scales. Here, each region’s quarterly sales are colored relative to that region’s own lowest and highest quarter:
quarterly_df = pd.DataFrame(
{
"region": ["North", "South", "East"],
"q1": [120, 45, 300],
"q2": [135, 52, 280],
"q3": [128, 61, 310],
"q4": [150, 58, 295],
}
)
GT(quarterly_df, rowname_col="region").data_color(palette="Blues", direction="row")
|
q1 |
q2 |
q3 |
q4 |
| North |
120 |
135 |
128 |
150 |
| South |
45 |
52 |
61 |
58 |
| East |
300 |
280 |
310 |
295 |
To pin more than the midpoint, we can build the function with col_numeric() and its stops= argument, which gives a value for each palette color. A stop can be a data value or a percentage of the way through the range of values in the column. Here, the lowest value is red, zero is white, and the highest value is green, so both sides use their full range of colors:
from great_tables import col_numeric
(
GT(df)
.data_color(
columns="change",
fn=col_numeric(palette=["#D7191C", "white", "#1A9641"], stops=["0%", 0, "100%"]),
)
.fmt_number(columns="change", decimals=1, force_sign=True)
)
| region |
change |
| North |
+12.5 |
| South |
−8.1 |
| East |
0.0 |
| West |
+3.2 |
| Central |
None |