diff --git a/doc/source/whatsnew/v0.20.0.rst b/doc/source/whatsnew/v0.20.0.rst index 5dac3a26424a8..ad8a23882e1e8 100644 --- a/doc/source/whatsnew/v0.20.0.rst +++ b/doc/source/whatsnew/v0.20.0.rst @@ -374,7 +374,7 @@ For example, after running the following, ``styled.xlsx`` renders as below: df.iloc[0, 2] = np.nan df styled = (df.style - .applymap(lambda val: 'color: %s' % 'red' if val < 0 else 'black') + .applymap(lambda val: 'color:red;' if val < 0 else 'color:black;') .highlight_max()) styled.to_excel('styled.xlsx', engine='openpyxl') diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index bf67ff6525005..1e9a96b536974 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -65,7 +65,8 @@ Other enhancements - :meth:`DataFrame.plot.scatter` can now accept a categorical column as the argument to ``c`` (:issue:`12380`, :issue:`31357`) - :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`, :issue:`21266`, :issue:`39317`) - :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`) -- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None``. (:issue:`39359`) +- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`) +- :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`) - :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`) - :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files. - Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index adfe1f285f2ee..1ec2f7bfdd4be 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -773,7 +773,8 @@ def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]: series = self.df.iloc[:, colidx] for i, val in enumerate(series): if styles is not None: - xlstyle = self.style_converter(";".join(styles[i, colidx])) + css = ";".join([a + ":" + str(v) for (a, v) in styles[i, colidx]]) + xlstyle = self.style_converter(css) yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle) def get_formatted_cells(self) -> Iterable[ExcelCell]: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1e2148125a9d1..c3218ccb91f25 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -53,8 +53,10 @@ ) jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") -CSSSequence = Sequence[Tuple[str, Union[str, int, float]]] -CSSProperties = Union[str, CSSSequence] + +CSSPair = Tuple[str, Union[str, int, float]] +CSSList = List[CSSPair] +CSSProperties = Union[str, CSSList] CSSStyles = List[Dict[str, CSSProperties]] try: @@ -194,7 +196,7 @@ def __init__( # assign additional default vars self.hidden_index: bool = False self.hidden_columns: Sequence[int] = [] - self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list) + self.ctx: DefaultDict[Tuple[int, int], CSSList] = defaultdict(list) self.cell_context: Dict[str, Any] = {} self._todo: List[Tuple[Callable, Tuple, Dict]] = [] self.tooltips: Optional[_Tooltips] = None @@ -414,7 +416,8 @@ def _translate(self): clabels = [[x] for x in clabels] clabels = list(zip(*clabels)) - cellstyle_map = defaultdict(list) + cellstyle_map: DefaultDict[Tuple[CSSPair, ...], List[str]] = defaultdict(list) + head = [] for r in range(n_clvls): @@ -531,25 +534,21 @@ def _translate(self): } # only add an id if the cell has a style - props = [] + props: CSSList = [] if self.cell_ids or (r, c) in ctx: row_dict["id"] = "_".join(cs[1:]) - for x in ctx[r, c]: - # have to handle empty styles like [''] - if x.count(":"): - props.append(tuple(x.split(":"))) - else: - props.append(("", "")) + props.extend(ctx[r, c]) # add custom classes from cell context cs.extend(cell_context.get("data", {}).get(r, {}).get(c, [])) row_dict["class"] = " ".join(cs) row_es.append(row_dict) - cellstyle_map[tuple(props)].append(f"row{r}_col{c}") + if props: # (), [] won't be in cellstyle_map, cellstyle respectively + cellstyle_map[tuple(props)].append(f"row{r}_col{c}") body.append(row_es) - cellstyle = [ + cellstyle: List[Dict[str, Union[CSSList, List[str]]]] = [ {"props": list(props), "selectors": selectors} for props, selectors in cellstyle_map.items() ] @@ -755,19 +754,14 @@ def render(self, **kwargs) -> str: self._compute() # TODO: namespace all the pandas keys d = self._translate() - # filter out empty styles, every cell will have a class - # but the list of props may just be [['', '']]. - # so we have the nested anys below - trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])] - d["cellstyle"] = trimmed d.update(kwargs) return self.template.render(**d) def _update_ctx(self, attrs: DataFrame) -> None: """ - Update the state of the Styler. + Update the state of the Styler for data cells. - Collects a mapping of {index_label: [': ']}. + Collects a mapping of {index_label: [('', ''), ..]}. Parameters ---------- @@ -776,20 +770,13 @@ def _update_ctx(self, attrs: DataFrame) -> None: Whitespace shouldn't matter and the final trailing ';' shouldn't matter. """ - coli = {k: i for i, k in enumerate(self.columns)} - rowi = {k: i for i, k in enumerate(self.index)} - for jj in range(len(attrs.columns)): - cn = attrs.columns[jj] - j = coli[cn] + for cn in attrs.columns: for rn, c in attrs[[cn]].itertuples(): if not c: continue - c = c.rstrip(";") - if not c: - continue - i = rowi[rn] - for pair in c.split(";"): - self.ctx[(i, j)].append(pair) + css_list = _maybe_convert_css_to_tuples(c) + i, j = self.index.get_loc(rn), self.columns.get_loc(cn) + self.ctx[(i, j)].extend(css_list) def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( @@ -2068,7 +2055,7 @@ def _maybe_wrap_formatter( raise TypeError(msg) -def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSSequence: +def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: """ Convert css-string to sequence of tuples format if needed. 'color:red; border:1px solid black;' -> [('color', 'red'), diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index b8df18d593667..4304ccc94cc7f 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -19,6 +19,19 @@ ) +def bar_grad(a=None, b=None, c=None, d=None): + """Used in multiple tests to simplify formatting of expected result""" + ret = [("width", "10em"), ("height", "80%")] + if all(x is None for x in [a, b, c, d]): + return ret + return ret + [ + ( + "background", + f"linear-gradient(90deg,{','.join(x for x in [a, b, c, d] if x)})", + ) + ] + + class TestStyler: def setup_method(self, method): np.random.seed(24) @@ -61,24 +74,15 @@ def test_repr_html_mathjax(self): def test_update_ctx(self): self.styler._update_ctx(self.attrs) - expected = {(0, 0): ["color: red"], (1, 0): ["color: blue"]} + expected = {(0, 0): [("color", "red")], (1, 0): [("color", "blue")]} assert self.styler.ctx == expected - def test_update_ctx_flatten_multi(self): - attrs = DataFrame({"A": ["color: red; foo: bar", "color: blue; foo: baz"]}) + def test_update_ctx_flatten_multi_and_trailing_semi(self): + attrs = DataFrame({"A": ["color: red; foo: bar", "color:blue ; foo: baz;"]}) self.styler._update_ctx(attrs) expected = { - (0, 0): ["color: red", " foo: bar"], - (1, 0): ["color: blue", " foo: baz"], - } - assert self.styler.ctx == expected - - def test_update_ctx_flatten_multi_traliing_semi(self): - attrs = DataFrame({"A": ["color: red; foo: bar;", "color: blue; foo: baz;"]}) - self.styler._update_ctx(attrs) - expected = { - (0, 0): ["color: red", " foo: bar"], - (1, 0): ["color: blue", " foo: baz"], + (0, 0): [("color", "red"), ("foo", "bar")], + (1, 0): [("color", "blue"), ("foo", "baz")], } assert self.styler.ctx == expected @@ -141,7 +145,7 @@ def test_multiple_render(self): s.render() # do 2 renders to ensure css styles not duplicated assert ( '" in s.render() + " color: red;\n}\n" in s.render() ) def test_render_empty_dfs(self): @@ -167,7 +171,7 @@ def test_set_properties(self): df = DataFrame({"A": [0, 1]}) result = df.style.set_properties(color="white", size="10px")._compute().ctx # order is deterministic - v = ["color: white", "size: 10px"] + v = [("color", "white"), ("size", "10px")] expected = {(0, 0): v, (1, 0): v} assert result.keys() == expected.keys() for v1, v2 in zip(result.values(), expected.values()): @@ -180,7 +184,7 @@ def test_set_properties_subset(self): ._compute() .ctx ) - expected = {(0, 0): ["color: white"]} + expected = {(0, 0): [("color", "white")]} assert result == expected def test_empty_index_name_doesnt_display(self): @@ -313,19 +317,19 @@ def test_apply_axis(self): assert len(result.ctx) == 0 result._compute() expected = { - (0, 0): ["val: 1"], - (0, 1): ["val: 1"], - (1, 0): ["val: 1"], - (1, 1): ["val: 1"], + (0, 0): [("val", "1")], + (0, 1): [("val", "1")], + (1, 0): [("val", "1")], + (1, 1): [("val", "1")], } assert result.ctx == expected result = df.style.apply(f, axis=0) expected = { - (0, 0): ["val: 0"], - (0, 1): ["val: 1"], - (1, 0): ["val: 0"], - (1, 1): ["val: 1"], + (0, 0): [("val", "0")], + (0, 1): [("val", "1")], + (1, 0): [("val", "0")], + (1, 1): [("val", "1")], } result._compute() assert result.ctx == expected @@ -333,53 +337,52 @@ def test_apply_axis(self): result._compute() assert result.ctx == expected - def test_apply_subset(self): - axes = [0, 1] - slices = [ + @pytest.mark.parametrize( + "slice_", + [ pd.IndexSlice[:], pd.IndexSlice[:, ["A"]], pd.IndexSlice[[1], :], pd.IndexSlice[[1], ["A"]], pd.IndexSlice[:2, ["A", "B"]], - ] - for ax in axes: - for slice_ in slices: - result = ( - self.df.style.apply(self.h, axis=ax, subset=slice_, foo="baz") - ._compute() - .ctx - ) - expected = { - (r, c): ["color: baz"] - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index - and col in self.df.loc[slice_].columns - } - assert result == expected - - def test_applymap_subset(self): - def f(x): - return "foo: bar" + ], + ) + @pytest.mark.parametrize("axis", [0, 1]) + def test_apply_subset(self, slice_, axis): + result = ( + self.df.style.apply(self.h, axis=axis, subset=slice_, foo="baz") + ._compute() + .ctx + ) + expected = { + (r, c): [("color", "baz")] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and col in self.df.loc[slice_].columns + } + assert result == expected - slices = [ + @pytest.mark.parametrize( + "slice_", + [ pd.IndexSlice[:], pd.IndexSlice[:, ["A"]], pd.IndexSlice[[1], :], pd.IndexSlice[[1], ["A"]], pd.IndexSlice[:2, ["A", "B"]], - ] - - for slice_ in slices: - result = self.df.style.applymap(f, subset=slice_)._compute().ctx - expected = { - (r, c): ["foo: bar"] - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index - and col in self.df.loc[slice_].columns - } - assert result == expected + ], + ) + def test_applymap_subset(self, slice_): + result = ( + self.df.style.applymap(lambda x: "color:baz;", subset=slice_)._compute().ctx + ) + expected = { + (r, c): [("color", "baz")] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and col in self.df.loc[slice_].columns + } + assert result == expected @pytest.mark.parametrize( "slice_", @@ -430,14 +433,24 @@ def f(x): result = self.df.style.where(f, style1)._compute().ctx expected = { - (r, c): [style1] + (r, c): [("foo", "bar")] for r, row in enumerate(self.df.index) for c, col in enumerate(self.df.columns) if f(self.df.loc[row, col]) } assert result == expected - def test_where_subset(self): + @pytest.mark.parametrize( + "slice_", + [ + pd.IndexSlice[:], + pd.IndexSlice[:, ["A"]], + pd.IndexSlice[[1], :], + pd.IndexSlice[[1], ["A"]], + pd.IndexSlice[:2, ["A", "B"]], + ], + ) + def test_where_subset(self, slice_): # GH 17474 def f(x): return x > 0.5 @@ -445,26 +458,14 @@ def f(x): style1 = "foo: bar" style2 = "baz: foo" - slices = [ - pd.IndexSlice[:], - pd.IndexSlice[:, ["A"]], - pd.IndexSlice[[1], :], - pd.IndexSlice[[1], ["A"]], - pd.IndexSlice[:2, ["A", "B"]], - ] - - for slice_ in slices: - result = ( - self.df.style.where(f, style1, style2, subset=slice_)._compute().ctx - ) - expected = { - (r, c): [style1 if f(self.df.loc[row, col]) else style2] - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index - and col in self.df.loc[slice_].columns - } - assert result == expected + result = self.df.style.where(f, style1, style2, subset=slice_)._compute().ctx + expected = { + (r, c): [("foo", "bar") if f(self.df.loc[row, col]) else ("baz", "foo")] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and col in self.df.loc[slice_].columns + } + assert result == expected def test_where_subset_compare_with_applymap(self): # GH 17474 @@ -495,11 +496,11 @@ def g(x): def test_empty(self): df = DataFrame({"A": [1, 0]}) s = df.style - s.ctx = {(0, 0): ["color: red"], (1, 0): [""]} + s.ctx = {(0, 0): [("color", "red")], (1, 0): [("", "")]} result = s._translate()["cellstyle"] expected = [ - {"props": [("color", " red")], "selectors": ["row0_col0"]}, + {"props": [("color", "red")], "selectors": ["row0_col0"]}, {"props": [("", "")], "selectors": ["row1_col0"]}, ] assert result == expected @@ -507,11 +508,11 @@ def test_empty(self): def test_duplicate(self): df = DataFrame({"A": [1, 0]}) s = df.style - s.ctx = {(0, 0): ["color: red"], (1, 0): ["color: red"]} + s.ctx = {(0, 0): [("color", "red")], (1, 0): [("color", "red")]} result = s._translate()["cellstyle"] expected = [ - {"props": [("color", " red")], "selectors": ["row0_col0", "row1_col0"]} + {"props": [("color", "red")], "selectors": ["row0_col0", "row1_col0"]} ] assert result == expected @@ -519,35 +520,17 @@ def test_bar_align_left(self): df = DataFrame({"A": [0, 1, 2]}) result = df.style.bar()._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(" - "90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(" - "90deg,#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 0): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected result = df.style.bar(color="red", width=50)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,red 25.0%, transparent 25.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,red 50.0%, transparent 50.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad("red 25.0%", " transparent 25.0%"), + (2, 0): bar_grad("red 50.0%", " transparent 50.0%"), } assert result == expected @@ -562,118 +545,54 @@ def test_bar_align_left_0points(self): df = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) result = df.style.bar()._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (0, 1): ["width: 10em", " height: 80%"], - (0, 2): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (2, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (2, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], + (0, 0): bar_grad(), + (0, 1): bar_grad(), + (0, 2): bar_grad(), + (1, 0): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 2): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 0): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 1): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected result = df.style.bar(axis=1)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (0, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (1, 0): ["width: 10em", " height: 80%"], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%" - ", transparent 50.0%)", - ], - (1, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (2, 0): ["width: 10em", " height: 80%"], - (2, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%" - ", transparent 50.0%)", - ], - (2, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], + (0, 0): bar_grad(), + (0, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (0, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (1, 0): bar_grad(), + (1, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 0): bar_grad(), + (2, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected def test_bar_align_mid_pos_and_neg(self): df = DataFrame({"A": [-10, 0, 20, 90]}) - result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx - expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 10.0%, transparent 10.0%)", - ], - (1, 0): ["width: 10em", " height: 80%"], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 10.0%, #5fba7d 10.0%" - ", #5fba7d 30.0%, transparent 30.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 10.0%, " - "#5fba7d 10.0%, #5fba7d 100.0%, " - "transparent 100.0%)", - ], + (0, 0): bar_grad( + "#d65f5f 10.0%", + " transparent 10.0%", + ), + (1, 0): bar_grad(), + (2, 0): bar_grad( + " transparent 10.0%", + " #5fba7d 10.0%", + " #5fba7d 30.0%", + " transparent 30.0%", + ), + (3, 0): bar_grad( + " transparent 10.0%", + " #5fba7d 10.0%", + " #5fba7d 100.0%", + " transparent 100.0%", + ), } - assert result == expected def test_bar_align_mid_all_pos(self): @@ -682,30 +601,22 @@ def test_bar_align_mid_all_pos(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 10.0%, transparent 10.0%)", - ], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 20.0%, transparent 20.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 50.0%, transparent 50.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad( + "#5fba7d 10.0%", + " transparent 10.0%", + ), + (1, 0): bar_grad( + "#5fba7d 20.0%", + " transparent 20.0%", + ), + (2, 0): bar_grad( + "#5fba7d 50.0%", + " transparent 50.0%", + ), + (3, 0): bar_grad( + "#5fba7d 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -716,36 +627,28 @@ def test_bar_align_mid_all_neg(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 100.0%, transparent 100.0%)", - ], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 40.0%, " - "#d65f5f 40.0%, #d65f5f 100.0%, " - "transparent 100.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 70.0%, " - "#d65f5f 70.0%, #d65f5f 100.0%, " - "transparent 100.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 80.0%, " - "#d65f5f 80.0%, #d65f5f 100.0%, " - "transparent 100.0%)", - ], + (0, 0): bar_grad( + "#d65f5f 100.0%", + " transparent 100.0%", + ), + (1, 0): bar_grad( + " transparent 40.0%", + " #d65f5f 40.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), + (2, 0): bar_grad( + " transparent 70.0%", + " #d65f5f 70.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), + (3, 0): bar_grad( + " transparent 80.0%", + " #d65f5f 80.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -759,28 +662,25 @@ def test_bar_align_zero_pos_and_neg(self): .ctx ) expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 40.0%, #d65f5f 40.0%, " - "#d65f5f 45.0%, transparent 45.0%)", - ], - (1, 0): ["width: 10em", " height: 80%"], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 45.0%, #5fba7d 45.0%, " - "#5fba7d 55.0%, transparent 55.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 45.0%, #5fba7d 45.0%, " - "#5fba7d 90.0%, transparent 90.0%)", - ], + (0, 0): bar_grad( + " transparent 40.0%", + " #d65f5f 40.0%", + " #d65f5f 45.0%", + " transparent 45.0%", + ), + (1, 0): bar_grad(), + (2, 0): bar_grad( + " transparent 45.0%", + " #5fba7d 45.0%", + " #5fba7d 55.0%", + " transparent 55.0%", + ), + (3, 0): bar_grad( + " transparent 45.0%", + " #5fba7d 45.0%", + " #5fba7d 90.0%", + " transparent 90.0%", + ), } assert result == expected @@ -788,25 +688,19 @@ def test_bar_align_left_axis_none(self): df = DataFrame({"A": [0, 1], "B": [2, 4]}) result = df.style.bar(axis=None)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 25.0%, transparent 25.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + "#d65f5f 25.0%", + " transparent 25.0%", + ), + (0, 1): bar_grad( + "#d65f5f 50.0%", + " transparent 50.0%", + ), + (1, 1): bar_grad( + "#d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -814,28 +708,25 @@ def test_bar_align_zero_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 62.5%, transparent 62.5%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 62.5%", + " transparent 62.5%", + ), + (0, 1): bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (1, 1): bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -843,27 +734,23 @@ def test_bar_align_mid_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 33.3%, #d65f5f 33.3%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 33.3%, transparent 33.3%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 33.3%, #d65f5f 33.3%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + " transparent 33.3%", + " #d65f5f 33.3%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (0, 1): bar_grad( + "#d65f5f 33.3%", + " transparent 33.3%", + ), + (1, 1): bar_grad( + " transparent 33.3%", + " #d65f5f 33.3%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -871,28 +758,25 @@ def test_bar_align_mid_vmin(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-6)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 60.0%, #d65f5f 60.0%, " - "#d65f5f 70.0%, transparent 70.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 40.0%, #d65f5f 40.0%, " - "#d65f5f 60.0%, transparent 60.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 60.0%, #d65f5f 60.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + " transparent 60.0%", + " #d65f5f 60.0%", + " #d65f5f 70.0%", + " transparent 70.0%", + ), + (0, 1): bar_grad( + " transparent 40.0%", + " #d65f5f 40.0%", + " #d65f5f 60.0%", + " transparent 60.0%", + ), + (1, 1): bar_grad( + " transparent 60.0%", + " #d65f5f 60.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -900,27 +784,23 @@ def test_bar_align_mid_vmax(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmax=8)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 20.0%, #d65f5f 20.0%, " - "#d65f5f 30.0%, transparent 30.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 20.0%, transparent 20.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 20.0%, #d65f5f 20.0%, " - "#d65f5f 60.0%, transparent 60.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + " transparent 20.0%", + " #d65f5f 20.0%", + " #d65f5f 30.0%", + " transparent 30.0%", + ), + (0, 1): bar_grad( + "#d65f5f 20.0%", + " transparent 20.0%", + ), + (1, 1): bar_grad( + " transparent 20.0%", + " #d65f5f 20.0%", + " #d65f5f 60.0%", + " transparent 60.0%", + ), } assert result == expected @@ -928,28 +808,25 @@ def test_bar_align_mid_vmin_vmax_wide(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-3, vmax=7)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 30.0%, #d65f5f 30.0%, " - "#d65f5f 40.0%, transparent 40.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 10.0%, #d65f5f 10.0%, " - "#d65f5f 30.0%, transparent 30.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 30.0%, #d65f5f 30.0%, " - "#d65f5f 70.0%, transparent 70.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + " transparent 30.0%", + " #d65f5f 30.0%", + " #d65f5f 40.0%", + " transparent 40.0%", + ), + (0, 1): bar_grad( + " transparent 10.0%", + " #d65f5f 10.0%", + " #d65f5f 30.0%", + " transparent 30.0%", + ), + (1, 1): bar_grad( + " transparent 30.0%", + " #d65f5f 30.0%", + " #d65f5f 70.0%", + " transparent 70.0%", + ), } assert result == expected @@ -957,27 +834,20 @@ def test_bar_align_mid_vmin_vmax_clipping(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-1, vmax=3)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 25.0%, transparent 25.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad(), + (1, 0): bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (0, 1): bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (1, 1): bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -985,26 +855,19 @@ def test_bar_align_mid_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 3]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 25.0%, transparent 25.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (0, 1): bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (1, 1): bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -1012,27 +875,24 @@ def test_bar_align_zero_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 2]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 75.0%, transparent 75.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 75.0%", + " transparent 75.0%", + ), + (0, 1): bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (1, 1): bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -1115,7 +975,7 @@ def test_format_with_bad_na_rep(self): def test_highlight_null(self, null_color="red"): df = DataFrame({"A": [0, np.nan]}) result = df.style.highlight_null()._compute().ctx - expected = {(1, 0): ["background-color: red"]} + expected = {(1, 0): [("background-color", "red")]} assert result == expected def test_highlight_null_subset(self): @@ -1128,8 +988,8 @@ def test_highlight_null_subset(self): .ctx ) expected = { - (1, 0): ["background-color: red"], - (1, 1): ["background-color: green"], + (1, 0): [("background-color", "red")], + (1, 1): [("background-color", "green")], } assert result == expected @@ -1228,7 +1088,7 @@ def f(x): ) result = DataFrame([[1, 2], [3, 4]]).style.apply(f, axis=None)._compute().ctx - assert result[(1, 1)] == ["color: red"] + assert result[(1, 1)] == [("color", "red")] def test_trim(self): result = self.df.style.render() # trim=True @@ -1239,6 +1099,7 @@ def test_trim(self): def test_highlight_max(self): df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) + css_seq = [("background-color", "yellow")] # max(df) = min(-df) for max_ in [True, False]: if max_: @@ -1247,35 +1108,35 @@ def test_highlight_max(self): df = -df attr = "highlight_min" result = getattr(df.style, attr)()._compute().ctx - assert result[(1, 1)] == ["background-color: yellow"] + assert result[(1, 1)] == css_seq result = getattr(df.style, attr)(color="green")._compute().ctx - assert result[(1, 1)] == ["background-color: green"] + assert result[(1, 1)] == [("background-color", "green")] result = getattr(df.style, attr)(subset="A")._compute().ctx - assert result[(1, 0)] == ["background-color: yellow"] + assert result[(1, 0)] == css_seq result = getattr(df.style, attr)(axis=0)._compute().ctx expected = { - (1, 0): ["background-color: yellow"], - (1, 1): ["background-color: yellow"], + (1, 0): css_seq, + (1, 1): css_seq, } assert result == expected result = getattr(df.style, attr)(axis=1)._compute().ctx expected = { - (0, 1): ["background-color: yellow"], - (1, 1): ["background-color: yellow"], + (0, 1): css_seq, + (1, 1): css_seq, } assert result == expected # separate since we can't negate the strs df["C"] = ["a", "b"] result = df.style.highlight_max()._compute().ctx - expected = {(1, 1): ["background-color: yellow"]} + expected = {(1, 1): css_seq} result = df.style.highlight_min()._compute().ctx - expected = {(0, 0): ["background-color: yellow"]} + expected = {(0, 0): css_seq} def test_export(self): f = lambda x: "color: red" if x > 0 else "color: blue" @@ -1983,7 +1844,7 @@ def test_background_gradient(self): for c_map in [None, "YlOrRd"]: result = df.style.background_gradient(cmap=c_map)._compute().ctx - assert all("#" in x[0] for x in result.values()) + assert all("#" in x[0][1] for x in result.values()) assert result[(0, 0)] == result[(0, 1)] assert result[(1, 0)] == result[(1, 1)] @@ -1991,7 +1852,7 @@ def test_background_gradient(self): df.style.background_gradient(subset=pd.IndexSlice[1, "A"])._compute().ctx ) - assert result[(1, 0)] == ["background-color: #fff7fb", "color: #000000"] + assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")] @pytest.mark.parametrize( "c_map,expected", @@ -1999,15 +1860,15 @@ def test_background_gradient(self): ( None, { - (0, 0): ["background-color: #440154", "color: #f1f1f1"], - (1, 0): ["background-color: #fde725", "color: #000000"], + (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"], + (0, 0): [("background-color", "#ffffcc"), ("color", "#000000")], + (1, 0): [("background-color", "#800026"), ("color", "#f1f1f1")], }, ), ], @@ -2030,9 +1891,9 @@ def test_text_color_threshold_raises(self, text_color_threshold): def test_background_gradient_axis(self): df = 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"] + 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